diff --git a/datetime/src/main/java/tools/jackson/datatype/jsr310/JavaTimeModule.java b/datetime/src/main/java/tools/jackson/datatype/jsr310/JavaTimeModule.java index 8515814a..0e5c8d82 100644 --- a/datetime/src/main/java/tools/jackson/datatype/jsr310/JavaTimeModule.java +++ b/datetime/src/main/java/tools/jackson/datatype/jsr310/JavaTimeModule.java @@ -19,6 +19,7 @@ import java.time.*; import tools.jackson.core.Version; +import tools.jackson.core.util.JacksonFeatureSet; import tools.jackson.databind.*; import tools.jackson.databind.deser.ValueInstantiator; import tools.jackson.databind.deser.ValueInstantiators; @@ -85,6 +86,9 @@ public final class JavaTimeModule { private static final long serialVersionUID = 1L; + private JacksonFeatureSet _features + = JacksonFeatureSet.fromDefaults(JavaTimeFeature.values()); + @Override public String getModuleName() { return getClass().getName(); @@ -97,13 +101,26 @@ public Version version() { public JavaTimeModule() { } + public JavaTimeModule enable(JavaTimeFeature f) { + _features = _features.with(f); + return this; + } + + public JavaTimeModule disable(JavaTimeFeature f) { + _features = _features.without(f); + return this; + } + @Override public void setupModule(SetupContext context) { context.addDeserializers(new SimpleDeserializers() // // Instant variants: - .addDeserializer(Instant.class, InstantDeserializer.INSTANT) - .addDeserializer(OffsetDateTime.class, InstantDeserializer.OFFSET_DATE_TIME) - .addDeserializer(ZonedDateTime.class, InstantDeserializer.ZONED_DATE_TIME) + .addDeserializer(Instant.class, + InstantDeserializer.INSTANT.withFeatures(_features)) + .addDeserializer(OffsetDateTime.class, + InstantDeserializer.OFFSET_DATE_TIME.withFeatures(_features)) + .addDeserializer(ZonedDateTime.class, + InstantDeserializer.ZONED_DATE_TIME.withFeatures(_features)) // // Other deserializers .addDeserializer(Duration.class, DurationDeserializer.INSTANCE) diff --git a/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java index 59850022..6351af76 100644 --- a/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/tools/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -17,11 +17,7 @@ package tools.jackson.datatype.jsr310.deser; import java.math.BigDecimal; -import java.time.DateTimeException; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; @@ -38,10 +34,11 @@ import tools.jackson.core.JsonToken; import tools.jackson.core.JsonTokenId; import tools.jackson.core.io.NumberInput; - +import tools.jackson.core.util.JacksonFeatureSet; import tools.jackson.databind.BeanProperty; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.DeserializationFeature; +import tools.jackson.datatype.jsr310.JavaTimeFeature; import tools.jackson.datatype.jsr310.util.DecimalUtils; /** @@ -49,11 +46,12 @@ * and {@link ZonedDateTime}s. * * @author Nick Williams - * @since 2.2 */ public class InstantDeserializer extends JSR310DateTimeDeserializerBase { + private final static boolean DEFAULT_NORMALIZE_ZONE_ID = JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID.enabledByDefault(); + /** * Constants used to check if ISO 8601 time string is colonless. See [jackson-modules-java8#131] */ @@ -66,7 +64,7 @@ public class InstantDeserializer a -> Instant.ofEpochSecond(a.integer, a.fraction), null, true, // yes, replace zero offset with Z - true // default: yes, normalize ZoneId + DEFAULT_NORMALIZE_ZONE_ID ); public static final InstantDeserializer OFFSET_DATE_TIME = new InstantDeserializer<>( @@ -76,7 +74,7 @@ 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 - true // default: yes, normalize ZoneId + DEFAULT_NORMALIZE_ZONE_ID ); public static final InstantDeserializer ZONED_DATE_TIME = new InstantDeserializer<>( @@ -86,7 +84,7 @@ 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 - true // default: yes, normalize ZoneId + DEFAULT_NORMALIZE_ZONE_ID ); protected final Function fromMilliseconds; @@ -208,6 +206,26 @@ protected InstantDeserializer(InstantDeserializer base, _normalizeZoneId = base._normalizeZoneId; } + /** + * @since 2.16 + */ + @SuppressWarnings("unchecked") + protected InstantDeserializer(InstantDeserializer base, + JacksonFeatureSet features) + { + super((Class) base.handledType(), base._formatter); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ; + _adjustToContextTZOverride = base._adjustToContextTZOverride; + _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride; + + _normalizeZoneId = features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID); + + } + @Override protected InstantDeserializer withDateFormat(DateTimeFormatter dtf) { if (dtf == _formatter) { @@ -221,6 +239,14 @@ protected InstantDeserializer withLeniency(Boolean leniency) { return new InstantDeserializer<>(this, _formatter, leniency); } + // @since 2.16 + public InstantDeserializer withFeatures(JacksonFeatureSet features) { + if (_normalizeZoneId == features.isEnabled(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) { + return this; + } + return new InstantDeserializer<>(this, features); + } + @SuppressWarnings("unchecked") @Override // @since 2.12.1 protected JSR310DateTimeDeserializerBase _withFormatOverrides(DeserializationContext ctxt, diff --git a/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java b/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java index 22d8ca17..50821ed7 100644 --- a/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java +++ b/datetime/src/test/java/tools/jackson/datatype/jsr310/deser/ZonedDateTimeDeserTest.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; +import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Feature; @@ -16,6 +17,9 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectReader; import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.datatype.jsr310.JavaTimeFeature; +import tools.jackson.datatype.jsr310.JavaTimeModule; import tools.jackson.datatype.jsr310.ModuleTestBase; import org.junit.Test; @@ -27,6 +31,12 @@ public class ZonedDateTimeDeserTest extends ModuleTestBase { private final ObjectReader READER = newMapper().readerFor(ZonedDateTime.class); + + private final ObjectReader READER_NON_NORMALIZED_ZONEID = JsonMapper.builder() + .addModule(new JavaTimeModule().disable(JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)) + .build() + .readerFor(ZonedDateTime.class); + private final TypeReference> MAP_TYPE_REF = new TypeReference>() { }; static class WrapperWithFeatures { @@ -55,13 +65,24 @@ public WrapperWithReadTimestampsAsNanosEnabled() { } } @Test - public void testDeserializationAsString01() throws Exception + public void testDeserFromString() throws Exception { assertEquals("The value is not correct.", ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), READER.readValue(q("2000-01-01T12:00Z"))); } + // [modules-java#281] + @Test + public void testDeserFromStringNoZoneIdNormalization() throws Exception + { + // 11-Nov-2023, tatu: Not sure this is great test but... does show diff + // behavior with and without `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID` + assertEquals("The value is not correct.", + ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, TimeZone.getTimeZone("UTC").toZoneId()), + READER_NON_NORMALIZED_ZONEID.readValue(q("2000-01-01T12:00Z"))); + } + @Test public void testDeserializationAsInt01() throws Exception { diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 3b32725e..b545e902 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -13,6 +13,9 @@ Modules: #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)