Skip to content

Commit

Permalink
Add feature toggle to read numeric strings as numeric timestamps (#269)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpkorstanje committed Nov 15, 2023
1 parent 5c9f0e0 commit ea1fc92
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 15 deletions.
Expand Up @@ -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.
* <p>
* 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;
Expand Down
Expand Up @@ -57,6 +57,8 @@ public class InstantDeserializer<T extends Temporal>
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]
Expand All @@ -72,7 +74,8 @@ public class InstantDeserializer<T extends Temporal>
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<OffsetDateTime> OFFSET_DATE_TIME = new InstantDeserializer<>(
Expand All @@ -82,7 +85,8 @@ public class InstantDeserializer<T extends Temporal>
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<ZonedDateTime> ZONED_DATE_TIME = new InstantDeserializer<>(
Expand All @@ -92,7 +96,8 @@ public class InstantDeserializer<T extends Temporal>
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<FromIntegerArguments, T> fromMilliseconds;
Expand Down Expand Up @@ -130,17 +135,34 @@ public class InstantDeserializer<T extends Temporal>
* 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}).
*<p>
* 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<T> supportedType,
DateTimeFormatter formatter,
Function<TemporalAccessor, T> parsedToValue,
Function<FromIntegerArguments, T> fromMilliseconds,
Function<FromDecimalArguments, T> fromNanoseconds,
BiFunction<T, ZoneId, T> adjust,
boolean replaceZeroOffsetAsZ,
boolean normalizeZoneId)
boolean normalizeZoneId,
boolean readNumericStringsAsTimestamp
)
{
super(supportedType, formatter);
this.parsedToValue = parsedToValue;
Expand All @@ -151,6 +173,7 @@ protected InstantDeserializer(Class<T> supportedType,
this._adjustToContextTZOverride = null;
this._readTimestampsAsNanosOverride = null;
_normalizeZoneId = normalizeZoneId;
_alwaysAllowStringifiedDateTimestamps = readNumericStringsAsTimestamp;
}

@SuppressWarnings("unchecked")
Expand All @@ -165,6 +188,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f)
_adjustToContextTZOverride = base._adjustToContextTZOverride;
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
_normalizeZoneId = base._normalizeZoneId;
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
}

@SuppressWarnings("unchecked")
Expand All @@ -179,6 +203,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, Boolean adjustToConte
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
_normalizeZoneId = base._normalizeZoneId;
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
}

@SuppressWarnings("unchecked")
Expand All @@ -193,6 +218,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f,
_adjustToContextTZOverride = base._adjustToContextTZOverride;
_readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
_normalizeZoneId = base._normalizeZoneId;
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
}

/**
Expand All @@ -214,6 +240,7 @@ protected InstantDeserializer(InstantDeserializer<T> base,
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
_readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
_normalizeZoneId = base._normalizeZoneId;
_alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
}

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

_normalizeZoneId = features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID);

_alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS);
}

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

// @since 2.16
public InstantDeserializer<T> withFeatures(JacksonFeatureSet<JavaTimeFeature> 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);
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down
6 changes: 6 additions & 0 deletions release-notes/CREDITS-2.x
Expand Up @@ -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)
12 changes: 9 additions & 3 deletions release-notes/VERSION-2.x
Expand Up @@ -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)
Expand Down

0 comments on commit ea1fc92

Please sign in to comment.