Skip to content

Commit ea1fc92

Browse files
authored
Add feature toggle to read numeric strings as numeric timestamps (#269)
1 parent 5c9f0e0 commit ea1fc92

File tree

5 files changed

+89
-15
lines changed

5 files changed

+89
-15
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@ public enum JavaTimeFeature implements JacksonFeature
1717
* Default setting is enabled, for backwards-compatibility with
1818
* Jackson 2.15.
1919
*/
20-
NORMALIZE_DESERIALIZED_ZONE_ID(true)
21-
;
20+
NORMALIZE_DESERIALIZED_ZONE_ID(true),
2221

2322
/**
23+
* Feature that controls whether stringified numbers (Strings that without
24+
* quotes would be legal JSON Numbers) may be interpreted as
25+
* timestamps (enabled) or not (disabled), in case where there is an
26+
* explicitly defined pattern ({@code DateTimeFormatter}) for value.
27+
* <p>
28+
* Note that when the default pattern is used (no custom pattern defined),
29+
* stringified numbers are always accepted as timestamps regardless of
30+
* this feature.
31+
*/
32+
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false)
33+
;
34+
35+
/**
2436
* Whether feature is enabled or disabled by default.
2537
*/
2638
private final boolean _defaultState;

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public class InstantDeserializer<T extends Temporal>
5757
private static final long serialVersionUID = 1L;
5858

5959
private final static boolean DEFAULT_NORMALIZE_ZONE_ID = JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID.enabledByDefault();
60+
private final static boolean DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
61+
= JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS.enabledByDefault();
6062

6163
/**
6264
* Constants used to check if ISO 8601 time string is colonless. See [jackson-modules-java8#131]
@@ -72,7 +74,8 @@ public class InstantDeserializer<T extends Temporal>
7274
a -> Instant.ofEpochSecond(a.integer, a.fraction),
7375
null,
7476
true, // yes, replace zero offset with Z
75-
DEFAULT_NORMALIZE_ZONE_ID
77+
DEFAULT_NORMALIZE_ZONE_ID,
78+
DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
7679
);
7780

7881
public static final InstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new InstantDeserializer<>(
@@ -82,7 +85,8 @@ public class InstantDeserializer<T extends Temporal>
8285
a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
8386
(d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))),
8487
true, // yes, replace zero offset with Z
85-
DEFAULT_NORMALIZE_ZONE_ID
88+
DEFAULT_NORMALIZE_ZONE_ID,
89+
DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
8690
);
8791

8892
public static final InstantDeserializer<ZonedDateTime> ZONED_DATE_TIME = new InstantDeserializer<>(
@@ -92,7 +96,8 @@ public class InstantDeserializer<T extends Temporal>
9296
a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
9397
ZonedDateTime::withZoneSameInstant,
9498
false, // keep zero offset and Z separate since zones explicitly supported
95-
DEFAULT_NORMALIZE_ZONE_ID
99+
DEFAULT_NORMALIZE_ZONE_ID,
100+
DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
96101
);
97102

98103
protected final Function<FromIntegerArguments, T> fromMilliseconds;
@@ -130,17 +135,34 @@ public class InstantDeserializer<T extends Temporal>
130135
* Flag set from
131136
* {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#NORMALIZE_DESERIALIZED_ZONE_ID} to
132137
* determine whether {@link ZoneId} is to be normalized during deserialization.
138+
*
139+
* @since 2.16
133140
*/
134141
protected final boolean _normalizeZoneId;
135142

143+
/**
144+
* Flag set from
145+
* {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS}
146+
* to determine whether stringified numbers are interpreted as timestamps
147+
* (enabled) nor not (disabled) in addition to a custom pattern ({code DateTimeFormatter}).
148+
*<p>
149+
* NOTE: stringified timestamps are always allowed with default patterns;
150+
* this flag only affects handling of custom patterns.
151+
*
152+
* @since 2.16
153+
*/
154+
protected final boolean _alwaysAllowStringifiedDateTimestamps;
155+
136156
protected InstantDeserializer(Class<T> supportedType,
137157
DateTimeFormatter formatter,
138158
Function<TemporalAccessor, T> parsedToValue,
139159
Function<FromIntegerArguments, T> fromMilliseconds,
140160
Function<FromDecimalArguments, T> fromNanoseconds,
141161
BiFunction<T, ZoneId, T> adjust,
142162
boolean replaceZeroOffsetAsZ,
143-
boolean normalizeZoneId)
163+
boolean normalizeZoneId,
164+
boolean readNumericStringsAsTimestamp
165+
)
144166
{
145167
super(supportedType, formatter);
146168
this.parsedToValue = parsedToValue;
@@ -151,6 +173,7 @@ protected InstantDeserializer(Class<T> supportedType,
151173
this._adjustToContextTZOverride = null;
152174
this._readTimestampsAsNanosOverride = null;
153175
_normalizeZoneId = normalizeZoneId;
176+
_alwaysAllowStringifiedDateTimestamps = readNumericStringsAsTimestamp;
154177
}
155178

156179
@SuppressWarnings("unchecked")
@@ -165,6 +188,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f)
165188
_adjustToContextTZOverride = base._adjustToContextTZOverride;
166189
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
167190
_normalizeZoneId = base._normalizeZoneId;
191+
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
168192
}
169193

170194
@SuppressWarnings("unchecked")
@@ -179,6 +203,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, Boolean adjustToConte
179203
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
180204
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
181205
_normalizeZoneId = base._normalizeZoneId;
206+
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
182207
}
183208

184209
@SuppressWarnings("unchecked")
@@ -193,6 +218,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f,
193218
_adjustToContextTZOverride = base._adjustToContextTZOverride;
194219
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
195220
_normalizeZoneId = base._normalizeZoneId;
221+
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
196222
}
197223

198224
/**
@@ -214,6 +240,7 @@ protected InstantDeserializer(InstantDeserializer<T> base,
214240
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
215241
_readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
216242
_normalizeZoneId = base._normalizeZoneId;
243+
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
217244
}
218245

219246
/**
@@ -233,7 +260,7 @@ protected InstantDeserializer(InstantDeserializer<T> base,
233260
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
234261

235262
_normalizeZoneId = features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID);
236-
263+
_alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS);
237264
}
238265

239266
@Override
@@ -251,7 +278,9 @@ protected InstantDeserializer<T> withLeniency(Boolean leniency) {
251278

252279
// @since 2.16
253280
public InstantDeserializer<T> withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
254-
if (_normalizeZoneId == features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) {
281+
if ((_normalizeZoneId == features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID))
282+
&& (_alwaysAllowStringifiedDateTimestamps == features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS))
283+
) {
255284
return this;
256285
}
257286
return new InstantDeserializer<>(this, features);
@@ -343,10 +372,12 @@ protected T _fromString(JsonParser p, DeserializationContext ctxt,
343372
// handled like "regular" empty (same as pre-2.12)
344373
return _fromEmptyString(p, ctxt, string);
345374
}
346-
// only check for other parsing modes if we are using default formatter
347-
if (_formatter == DateTimeFormatter.ISO_INSTANT ||
348-
_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME ||
349-
_formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) {
375+
// only check for other parsing modes if we are using default formatter or explicitly asked to
376+
if (_alwaysAllowStringifiedDateTimestamps ||
377+
_formatter == DateTimeFormatter.ISO_INSTANT ||
378+
_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME ||
379+
_formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME
380+
) {
350381
// 22-Jan-2016, [datatype-jsr310#16]: Allow quoted numbers too
351382
int dots = _countPeriods(string);
352383
if (dots >= 0) { // negative if not simple number

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/ZonedDateTimeSerTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
import java.util.Locale;
3333
import java.util.TimeZone;
3434

35+
import com.fasterxml.jackson.databind.json.JsonMapper;
36+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;
37+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3538
import org.junit.Test;
3639

3740
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -926,6 +929,22 @@ public void testCustomPatternWithAnnotations() throws Exception
926929
assertEquals(input.value.toInstant(), result.value.toInstant());
927930
}
928931

932+
// [modules-java#269]
933+
@Test
934+
public void testCustomPatternWithNumericTimestamp() throws Exception
935+
{
936+
String input = a2q("{'value':'3.141592653'}");
937+
938+
Wrapper result = JsonMapper.builder()
939+
.addModule(new JavaTimeModule()
940+
.enable(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS))
941+
.build()
942+
.readerFor(Wrapper.class)
943+
.readValue(input);
944+
945+
assertEquals(Instant.ofEpochSecond(3L, 141592653L), result.value.toInstant());
946+
}
947+
929948
@Test
930949
public void testNumericCustomPatternWithAnnotations() throws Exception
931950
{

release-notes/CREDITS-2.x

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,9 @@ Raman Babich (raman-babich@github)
179179
* Contributed fix for #272: `JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS`
180180
not respected when deserialising `Instant`s
181181
(2.16.0)
182+
183+
M.P. Korstanje (mpkorstanje@github)
184+
185+
* Contributed #263: Add `JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_TIMESTAMPS` to allow parsing
186+
quoted numbers when using a custom DateTimeFormatter
187+
(2.16.0)

release-notes/VERSION-2.x

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ Modules:
88
=== Releases ===
99
------------------------------------------------------------------------
1010

11+
Not yet released
12+
13+
#263: Add `JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_TIMESTAMPS` to allow parsing
14+
quoted numbers when using a custom pattern (DateTimeFormatter)
15+
(contributed by M.P. Korstanje)
16+
#281: (datetime) Add `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` to allow
17+
disabling ZoneId normalization on deserialization
18+
(requested by @indyana)
19+
1120
2.16.0-rc1 (20-Oct-2023)
1221

1322
#272: (datetime) `JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS`
1423
not respected when deserialising `Instant`s
1524
(fix contributed by Raman B)
16-
#281: (datetime) Add `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` to allow
17-
disabling ZoneId normalization on deserialization
18-
(requested by @indyana)
1925

2026
2.15.3 (12-Oct-2023)
2127
2.15.2 (30-May-2023)

0 commit comments

Comments
 (0)