Skip to content

Commit c4b3a88

Browse files
authored
Fix #6239: honor explicit NUMBER_INT/NUMBER_FLOAT shape for java.time nanoseconds (ser + deser) (#6238)
1 parent eebde32 commit c4b3a88

13 files changed

Lines changed: 242 additions & 22 deletions

‎release-notes/VERSION‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ Versions: 3.x (for earlier see VERSION-2.x)
8585
(fix by @cowtowncoder, w/ Claude code)
8686
#6233: Serialize `Month` as Enum name with `@JsonFormat(shape = STRING)`
8787
(fix by @pjfanning)
88+
#6239: Explicit `JsonFormat.Shape.NUMBER_INT`/`NUMBER_FLOAT` not honored for
89+
nanosecond handling of `java.time` values (serialization, deserialization)
90+
(fix by @cowtowncoder, w/ Claude code)
8891

8992
3.2.4 (not yet released)
9093

‎src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ public enum DateTimeFeature implements DatatypeFeature
127127
* If disabled, standard millisecond timestamps are assumed.
128128
* This is the counterpart to {@link DateTimeFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS}.
129129
*<p>
130+
* This is the global default: it may be overridden per-type or per-property with
131+
* {@code @JsonFormat} (or config override), either by explicit
132+
* {@code JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS}, or
133+
* (if no such feature is specified) by explicit numeric shape:
134+
* {@code JsonFormat.Shape.NUMBER_INT} means timestamps are read as milliseconds,
135+
* {@code JsonFormat.Shape.NUMBER_FLOAT} that they are read as nanoseconds
136+
* (since 3.3).
137+
*<p>
130138
* Feature used to be one of {@link tools.jackson.databind.DeserializationFeature}s
131139
* in Jackson 2.x but was moved here in 3.0.
132140
*<p>
@@ -238,6 +246,14 @@ public enum DateTimeFeature implements DatatypeFeature
238246
* If disabled, standard millisecond timestamps are assumed.
239247
* This is the counterpart to {@link DateTimeFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}.
240248
*<p>
249+
* This is the global default: it may be overridden per-type or per-property with
250+
* {@code @JsonFormat} (or config override), either by explicit
251+
* {@code JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS}, or
252+
* (if no such feature is specified) by explicit numeric shape:
253+
* {@code JsonFormat.Shape.NUMBER_INT} means timestamps are written as milliseconds,
254+
* {@code JsonFormat.Shape.NUMBER_FLOAT} that they are written as nanoseconds
255+
* (since 3.3).
256+
*<p>
241257
* Feature used to be one of {@link tools.jackson.databind.SerializationFeature}s
242258
* in Jackson 2.x but was moved here in 3.0.
243259
*<p>

‎src/main/java/tools/jackson/databind/ext/javatime/deser/DurationDeserializer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
117117
pattern, DurationUnitConverter.descForAllowed()));
118118
}
119119
}
120-
timestampsAsNanosOverride =
121-
format.getFeature(JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
120+
timestampsAsNanosOverride = _findReadTimestampsAsNanosOverride(format);
122121
}
123122
if (leniency != _isLenient
124123
|| !Objects.equals(unitConverter, _durationUnitConverter)

‎src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,7 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
238238
property, formatOverrides);
239239
Boolean adjustToContextTZOverride = formatOverrides.getFeature(
240240
JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
241-
Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
242-
JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
241+
Boolean readTimestampsAsNanosOverride = _findReadTimestampsAsNanosOverride(formatOverrides);
243242
if (!Objects.equals(adjustToContextTZOverride, deser._adjustToContextTZOverride)
244243
|| !Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
245244
return new InstantDeserializer<>(deser, deser._isLenient, deser._formatter,

‎src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.time.format.DateTimeFormatter;
2121
import java.util.Arrays;
2222

23+
import com.fasterxml.jackson.annotation.JsonFormat;
24+
2325
import tools.jackson.core.JacksonException;
2426
import tools.jackson.core.JsonParser;
2527
import tools.jackson.core.JsonToken;
@@ -76,6 +78,32 @@ protected JSR310DeserializerBase(JSR310DeserializerBase<T> base, Boolean lenienc
7678

7779
protected abstract JSR310DeserializerBase<T> withLeniency(Boolean leniency);
7880

81+
/**
82+
* Helper method for resolving per-property override for reading numeric timestamps
83+
* as nanoseconds (or not): explicit
84+
* {@link JsonFormat.Feature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS} has precedence;
85+
* if not defined, explicit numeric shape is used ({@link JsonFormat.Shape#NUMBER_INT}
86+
* meaning "not as nanoseconds", {@link JsonFormat.Shape#NUMBER_FLOAT} "as nanoseconds")
87+
* to match handling on serialization side.
88+
* Returns {@code null} if neither is defined (to use global default).
89+
*
90+
* @since 3.3
91+
*/
92+
protected static Boolean _findReadTimestampsAsNanosOverride(JsonFormat.Value format) {
93+
Boolean b = format.getFeature(JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
94+
if (b == null) {
95+
// [databind#6239]: match `JSR310FormattedSerializerBase.useNanoseconds()`
96+
switch (format.getShape()) {
97+
case NUMBER_INT:
98+
return Boolean.FALSE;
99+
case NUMBER_FLOAT:
100+
return Boolean.TRUE;
101+
default:
102+
}
103+
}
104+
return b;
105+
}
106+
79107
/**
80108
* @return {@code true} if lenient handling is enabled; {code false} if not (strict mode)
81109
*/

‎src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
8686
{
8787
LocalDateTimeDeserializer deser = (LocalDateTimeDeserializer)
8888
super._withFormatOverrides(ctxt, property, formatOverrides);
89-
Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
90-
JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
89+
Boolean readTimestampsAsNanosOverride = _findReadTimestampsAsNanosOverride(formatOverrides);
9190
if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
9291
return new LocalDateTimeDeserializer(deser, deser._isLenient, deser._formatter,
9392
deser._shape, readTimestampsAsNanosOverride);

‎src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
9393
{
9494
LocalTimeDeserializer deser = (LocalTimeDeserializer)
9595
super._withFormatOverrides(ctxt, property, formatOverrides);
96-
Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
97-
JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
96+
Boolean readTimestampsAsNanosOverride = _findReadTimestampsAsNanosOverride(formatOverrides);
9897
if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
9998
return new LocalTimeDeserializer(deser, deser._isLenient, deser._formatter,
10099
deser._shape, readTimestampsAsNanosOverride);

‎src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
8787
{
8888
OffsetTimeDeserializer deser = (OffsetTimeDeserializer)
8989
super._withFormatOverrides(ctxt, property, formatOverrides);
90-
Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
91-
JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
90+
Boolean readTimestampsAsNanosOverride = _findReadTimestampsAsNanosOverride(formatOverrides);
9291
if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
9392
return new OffsetTimeDeserializer(deser, deser._isLenient, deser._formatter,
9493
deser._shape, readTimestampsAsNanosOverride);

‎src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,13 @@ protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf,
6969

7070
protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf,
7171
Boolean useTimestamp, Boolean useNanoseconds) {
72-
super(base, dtf, useTimestamp, useNanoseconds, null);
72+
this(base, dtf, useTimestamp, useNanoseconds, null);
73+
}
74+
75+
// @since 3.3
76+
protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf,
77+
Boolean useTimestamp, Boolean useNanoseconds, JsonFormat.Shape shape) {
78+
super(base, dtf, useTimestamp, useNanoseconds, shape);
7379
}
7480

7581
protected DurationSerializer(DurationSerializer base, DurationUnitConverter converter) {
@@ -80,7 +86,7 @@ protected DurationSerializer(DurationSerializer base, DurationUnitConverter conv
8086
@Override
8187
protected DurationSerializer withFormat(DateTimeFormatter dtf,
8288
Boolean useTimestamp, JsonFormat.Shape shape) {
83-
return new DurationSerializer(this, dtf, useTimestamp);
89+
return new DurationSerializer(this, dtf, useTimestamp, null, shape);
8490
}
8591

8692
protected DurationSerializer withConverter(DurationUnitConverter converter) {
@@ -179,7 +185,7 @@ protected JsonToken serializationShape(SerializationContext ctxt) {
179185

180186
@Override
181187
protected JSR310FormattedSerializerBase<?> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
182-
return new DurationSerializer(this, _formatter, _useTimestamp, writeNanoseconds);
188+
return new DurationSerializer(this, _formatter, _useTimestamp, writeNanoseconds, _shape);
183189
}
184190

185191
@Override

‎src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,19 @@ public LocalDateTimeSerializer(DateTimeFormatter f) {
4949

5050
protected LocalDateTimeSerializer(LocalDateTimeSerializer base, DateTimeFormatter dtf,
5151
Boolean useTimestamp, Boolean useNanoseconds) {
52-
super(base, dtf, useTimestamp, useNanoseconds, null);
52+
this(base, dtf, useTimestamp, useNanoseconds, null);
53+
}
54+
55+
// @since 3.3
56+
protected LocalDateTimeSerializer(LocalDateTimeSerializer base, DateTimeFormatter dtf,
57+
Boolean useTimestamp, Boolean useNanoseconds, JsonFormat.Shape shape) {
58+
super(base, dtf, useTimestamp, useNanoseconds, shape);
5359
}
5460

5561
@Override
5662
protected JSR310FormattedSerializerBase<LocalDateTime> withFormat(DateTimeFormatter f,
5763
Boolean useTimestamp, JsonFormat.Shape shape) {
58-
return new LocalDateTimeSerializer(this, f, useTimestamp, _useNanoseconds);
64+
return new LocalDateTimeSerializer(this, f, useTimestamp, _useNanoseconds, shape);
5965
}
6066

6167
protected DateTimeFormatter _defaultFormatter() {
@@ -155,6 +161,6 @@ protected JsonToken serializationShape(SerializationContext ctxt) {
155161

156162
@Override
157163
protected JSR310FormattedSerializerBase<?> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
158-
return new LocalDateTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds);
164+
return new LocalDateTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds, _shape);
159165
}
160166
}

0 commit comments

Comments
 (0)