From ea1fc9288fa65efb2b62115696a0e6ec890a4b49 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Wed, 15 Nov 2023 06:01:37 +0100 Subject: [PATCH] Add feature toggle to read numeric strings as numeric timestamps (#269) --- .../datatype/jsr310/JavaTimeFeature.java | 16 +++++- .../jsr310/deser/InstantDeserializer.java | 51 +++++++++++++++---- .../jsr310/ser/ZonedDateTimeSerTest.java | 19 +++++++ release-notes/CREDITS-2.x | 6 +++ release-notes/VERSION-2.x | 12 +++-- 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java index d35c2303..014e8453 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeFeature.java @@ -17,10 +17,22 @@ public enum JavaTimeFeature implements JacksonFeature * Default setting is enabled, for backwards-compatibility with * Jackson 2.15. */ - NORMALIZE_DESERIALIZED_ZONE_ID(true) - ; + NORMALIZE_DESERIALIZED_ZONE_ID(true), /** + * Feature that controls whether stringified numbers (Strings that without + * quotes would be legal JSON Numbers) may be interpreted as + * timestamps (enabled) or not (disabled), in case where there is an + * explicitly defined pattern ({@code DateTimeFormatter}) for value. + *

+ * Note that when the default pattern is used (no custom pattern defined), + * stringified numbers are always accepted as timestamps regardless of + * this feature. + */ + ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false) + ; + + /** * Whether feature is enabled or disabled by default. */ private final boolean _defaultState; diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java index 1d1f4dfb..e52b585f 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -57,6 +57,8 @@ public class InstantDeserializer private static final long serialVersionUID = 1L; private final static boolean DEFAULT_NORMALIZE_ZONE_ID = JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID.enabledByDefault(); + private final static boolean DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS + = JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS.enabledByDefault(); /** * Constants used to check if ISO 8601 time string is colonless. See [jackson-modules-java8#131] @@ -72,7 +74,8 @@ public class InstantDeserializer a -> Instant.ofEpochSecond(a.integer, a.fraction), null, true, // yes, replace zero offset with Z - DEFAULT_NORMALIZE_ZONE_ID + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS ); public static final InstantDeserializer OFFSET_DATE_TIME = new InstantDeserializer<>( @@ -82,7 +85,8 @@ public class InstantDeserializer a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))), true, // yes, replace zero offset with Z - DEFAULT_NORMALIZE_ZONE_ID + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS ); public static final InstantDeserializer ZONED_DATE_TIME = new InstantDeserializer<>( @@ -92,7 +96,8 @@ public class InstantDeserializer a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), ZonedDateTime::withZoneSameInstant, false, // keep zero offset and Z separate since zones explicitly supported - DEFAULT_NORMALIZE_ZONE_ID + DEFAULT_NORMALIZE_ZONE_ID, + DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS ); protected final Function fromMilliseconds; @@ -130,9 +135,24 @@ public class InstantDeserializer * Flag set from * {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#NORMALIZE_DESERIALIZED_ZONE_ID} to * determine whether {@link ZoneId} is to be normalized during deserialization. + * + * @since 2.16 */ protected final boolean _normalizeZoneId; + /** + * Flag set from + * {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS} + * to determine whether stringified numbers are interpreted as timestamps + * (enabled) nor not (disabled) in addition to a custom pattern ({code DateTimeFormatter}). + *

+ * NOTE: stringified timestamps are always allowed with default patterns; + * this flag only affects handling of custom patterns. + * + * @since 2.16 + */ + protected final boolean _alwaysAllowStringifiedDateTimestamps; + protected InstantDeserializer(Class supportedType, DateTimeFormatter formatter, Function parsedToValue, @@ -140,7 +160,9 @@ protected InstantDeserializer(Class supportedType, Function fromNanoseconds, BiFunction adjust, boolean replaceZeroOffsetAsZ, - boolean normalizeZoneId) + boolean normalizeZoneId, + boolean readNumericStringsAsTimestamp + ) { super(supportedType, formatter); this.parsedToValue = parsedToValue; @@ -151,6 +173,7 @@ protected InstantDeserializer(Class supportedType, this._adjustToContextTZOverride = null; this._readTimestampsAsNanosOverride = null; _normalizeZoneId = normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = readNumericStringsAsTimestamp; } @SuppressWarnings("unchecked") @@ -165,6 +188,7 @@ protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f) _adjustToContextTZOverride = base._adjustToContextTZOverride; _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; } @SuppressWarnings("unchecked") @@ -179,6 +203,7 @@ protected InstantDeserializer(InstantDeserializer base, Boolean adjustToConte _adjustToContextTZOverride = adjustToContextTimezoneOverride; _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; } @SuppressWarnings("unchecked") @@ -193,6 +218,7 @@ protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f, _adjustToContextTZOverride = base._adjustToContextTZOverride; _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; } /** @@ -214,6 +240,7 @@ protected InstantDeserializer(InstantDeserializer base, _adjustToContextTZOverride = adjustToContextTimezoneOverride; _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride; _normalizeZoneId = base._normalizeZoneId; + _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps; } /** @@ -233,7 +260,7 @@ protected InstantDeserializer(InstantDeserializer base, _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; _normalizeZoneId = features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID); - + _alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS); } @Override @@ -251,7 +278,9 @@ protected InstantDeserializer withLeniency(Boolean leniency) { // @since 2.16 public InstantDeserializer withFeatures(JacksonFeatureSet features) { - if (_normalizeZoneId == features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) { + if ((_normalizeZoneId == features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) + && (_alwaysAllowStringifiedDateTimestamps == features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS)) + ) { return this; } return new InstantDeserializer<>(this, features); @@ -343,10 +372,12 @@ protected T _fromString(JsonParser p, DeserializationContext ctxt, // handled like "regular" empty (same as pre-2.12) return _fromEmptyString(p, ctxt, string); } - // only check for other parsing modes if we are using default formatter - if (_formatter == DateTimeFormatter.ISO_INSTANT || - _formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME || - _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) { + // only check for other parsing modes if we are using default formatter or explicitly asked to + if (_alwaysAllowStringifiedDateTimestamps || + _formatter == DateTimeFormatter.ISO_INSTANT || + _formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME || + _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME + ) { // 22-Jan-2016, [datatype-jsr310#16]: Allow quoted numbers too int dots = _countPeriods(string); if (dots >= 0) { // negative if not simple number diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/ZonedDateTimeSerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/ZonedDateTimeSerTest.java index fece304c..5c306770 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/ZonedDateTimeSerTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/ZonedDateTimeSerTest.java @@ -32,6 +32,9 @@ import java.util.Locale; import java.util.TimeZone; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.Test; import com.fasterxml.jackson.annotation.JsonFormat; @@ -926,6 +929,22 @@ public void testCustomPatternWithAnnotations() throws Exception assertEquals(input.value.toInstant(), result.value.toInstant()); } + // [modules-java#269] + @Test + public void testCustomPatternWithNumericTimestamp() throws Exception + { + String input = a2q("{'value':'3.141592653'}"); + + Wrapper result = JsonMapper.builder() + .addModule(new JavaTimeModule() + .enable(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS)) + .build() + .readerFor(Wrapper.class) + .readValue(input); + + assertEquals(Instant.ofEpochSecond(3L, 141592653L), result.value.toInstant()); + } + @Test public void testNumericCustomPatternWithAnnotations() throws Exception { diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 74ea1bce..1fdeb01e 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -179,3 +179,9 @@ Raman Babich (raman-babich@github) * Contributed fix for #272: `JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS` not respected when deserialising `Instant`s (2.16.0) + +M.P. Korstanje (mpkorstanje@github) + + * Contributed #263: Add `JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_TIMESTAMPS` to allow parsing + quoted numbers when using a custom DateTimeFormatter + (2.16.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index b545e902..6c3d2158 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,14 +8,20 @@ Modules: === Releases === ------------------------------------------------------------------------ +Not yet released + +#263: Add `JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_TIMESTAMPS` to allow parsing + quoted numbers when using a custom pattern (DateTimeFormatter) + (contributed by M.P. Korstanje) +#281: (datetime) Add `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` to allow + disabling ZoneId normalization on deserialization + (requested by @indyana) + 2.16.0-rc1 (20-Oct-2023) #272: (datetime) `JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS` not respected when deserialising `Instant`s (fix contributed by Raman B) -#281: (datetime) Add `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` to allow - disabling ZoneId normalization on deserialization - (requested by @indyana) 2.15.3 (12-Oct-2023) 2.15.2 (30-May-2023)