Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature toggle to read numeric strings as numeric timestamps #269

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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