From fb821d7c51822cb87dceb09dceb2388e56ac0497 Mon Sep 17 00:00:00 2001 From: Abdullah <89297042+AzazelSensei@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:20:54 +0000 Subject: [PATCH 1/2] Document implicit UTC vs ZonedDateTime serialization #303 ObjectMapper default TimeZone is UTC but hasExplicitTimeZone() stays false until setTimeZone is called, so ZonedDateTime keeps its own zone unless WRITE_DATES_WITH_CONTEXT_TIME_ZONE has an explicit timezone. --- datetime/README.md | 2 ++ .../jackson/datatype/jsr310/JavaTimeModule.java | 9 +++++++++ .../datatype/jsr310/ser/InstantSerializerBase.java | 9 +++++++++ 3 files changed, 20 insertions(+) diff --git a/datetime/README.md b/datetime/README.md index 3892400e..6b5ea593 100644 --- a/datetime/README.md +++ b/datetime/README.md @@ -37,6 +37,8 @@ For TimeZone handling, `ADJUST_DATES_TO_CONTEXT_TIME_ZONE` (default: true) speci 'SerializedProvider#getTimeZone()' should be used to adjust Date/Time values on deserialization, even if the value itself contains timezone information. The resultant ZoneId will be [normalized](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#normalized--) where applicable. If the value is `OffsetDateTime.MIN` or `OffsetDateTime.MAX`, the Date/Time value will not be adjusted. If disabled, it will only be used if the value itself does not contain any TimeZone information. +On serialization, `ObjectMapper`'s default TimeZone is UTC, but that default is **implicit** (`SerializationConfig.hasExplicitTimeZone()` is false until `ObjectMapper.setTimeZone(...)` or `ObjectWriter.with(TimeZone)` is called). `ZonedDateTime` and `OffsetDateTime` therefore keep the zone already on the value when written as ISO-8601 strings. `SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE` (enabled by default since 2.13) only converts to the mapper TimeZone when that timezone was set explicitly. Calling `mapper.setTimeZone(TimeZone.getTimeZone("UTC"))` is enough to make UTC take effect for those types. + Finally, there are two features that apply to array handling. `UNWRAP_SINGLE_VALUE_ARRAYS` (default: false) allows auto-conversion from single-element arrays to non-JSON-array values. If the JSON value contains more than one element in the array, deserialization will still fail. `ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT` (default: false) determines whether empty Array value ("[ ]" in JSON) is accepted as null value for regular POJOs ("beans") with data-binding diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java index 80ede099..46943009 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java @@ -61,6 +61,15 @@ * local dates and times, periods, durations, zones, and more. All {@code java.time} types * have built-in translation to and from ISO-8601 formats. *

+ * On serialization, {@code ObjectMapper}'s default TimeZone is UTC, but that + * default is implicit ({@code SerializationConfig.hasExplicitTimeZone()} is + * false until {@code ObjectMapper.setTimeZone} or {@code ObjectWriter.with(TimeZone)} + * is called). {@link ZonedDateTime} and {@link OffsetDateTime} therefore keep + * the zone already on the value when written as ISO-8601 strings. + * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATES_WITH_CONTEXT_TIME_ZONE} + * (enabled by default since 2.13) only converts to the mapper TimeZone when + * that timezone was set explicitly. + *

* Granularity of timestamps is controlled through the companion features * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} and * {@link com.fasterxml.jackson.databind.DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}. For serialization, timestamps are diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java index 81c3ebd8..2f986ff5 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java @@ -42,6 +42,15 @@ /** * Base class for serializers used for {@link java.time.Instant} and * other {@link Temporal} subtypes. + *

+ * When writing ISO-8601 strings, the mapper {@link java.util.TimeZone} is + * applied only if it was set explicitly + * ({@code SerializationConfig.hasExplicitTimeZone()} is true) and + * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATES_WITH_CONTEXT_TIME_ZONE} + * is enabled (the default since 2.13). {@code ObjectMapper}'s documented + * default of UTC is implicit until {@code setTimeZone} or + * {@code ObjectWriter.with(TimeZone)} is called, so {@link java.time.ZonedDateTime} + * keeps the zone already on the value. */ @SuppressWarnings("serial") public abstract class InstantSerializerBase From 083d9342eff30b3b641fbb4a84bdb0a062ed9ae3 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 20 Sep 2026 18:38:31 -0700 Subject: [PATCH 2/2] Minor touch ups --- datetime/README.md | 2 +- .../datatype/jsr310/JavaTimeModule.java | 12 +++------ .../jsr310/ser/InstantSerializerBase.java | 25 +++++++++++++------ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/datetime/README.md b/datetime/README.md index 6b5ea593..b53d6ba5 100644 --- a/datetime/README.md +++ b/datetime/README.md @@ -37,7 +37,7 @@ For TimeZone handling, `ADJUST_DATES_TO_CONTEXT_TIME_ZONE` (default: true) speci 'SerializedProvider#getTimeZone()' should be used to adjust Date/Time values on deserialization, even if the value itself contains timezone information. The resultant ZoneId will be [normalized](https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html#normalized--) where applicable. If the value is `OffsetDateTime.MIN` or `OffsetDateTime.MAX`, the Date/Time value will not be adjusted. If disabled, it will only be used if the value itself does not contain any TimeZone information. -On serialization, `ObjectMapper`'s default TimeZone is UTC, but that default is **implicit** (`SerializationConfig.hasExplicitTimeZone()` is false until `ObjectMapper.setTimeZone(...)` or `ObjectWriter.with(TimeZone)` is called). `ZonedDateTime` and `OffsetDateTime` therefore keep the zone already on the value when written as ISO-8601 strings. `SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE` (enabled by default since 2.13) only converts to the mapper TimeZone when that timezone was set explicitly. Calling `mapper.setTimeZone(TimeZone.getTimeZone("UTC"))` is enough to make UTC take effect for those types. +On serialization, `ObjectMapper`'s default TimeZone is UTC, but that default is **implicit** (`SerializationConfig.hasExplicitTimeZone()` is false until `ObjectMapper.setTimeZone(...)` or `ObjectWriter.with(TimeZone)` is called). `ZonedDateTime` and `OffsetDateTime` therefore keep the zone already on the value when written as ISO-8601 strings. `SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE` (enabled by default since 2.13) only converts to the mapper TimeZone when that timezone was set explicitly. Calling `mapper.setTimeZone(TimeZone.getTimeZone("UTC"))` is enough to make UTC take effect for those types, except when the value is written with `SerializationFeature.WRITE_DATES_WITH_ZONE_ID` enabled (`ZonedDateTime` then always writes its own zone id) or with a `@JsonFormat` pattern whose formatter already carries a timezone of its own. Finally, there are two features that apply to array handling. `UNWRAP_SINGLE_VALUE_ARRAYS` (default: false) allows auto-conversion from single-element arrays to non-JSON-array values. If the JSON value contains more than one element in the array, deserialization will still fail. `ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT` (default: false) determines whether empty Array value ("[ ]" in JSON) is accepted diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java index 46943009..e95c8748 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.java @@ -61,14 +61,10 @@ * local dates and times, periods, durations, zones, and more. All {@code java.time} types * have built-in translation to and from ISO-8601 formats. *

- * On serialization, {@code ObjectMapper}'s default TimeZone is UTC, but that - * default is implicit ({@code SerializationConfig.hasExplicitTimeZone()} is - * false until {@code ObjectMapper.setTimeZone} or {@code ObjectWriter.with(TimeZone)} - * is called). {@link ZonedDateTime} and {@link OffsetDateTime} therefore keep - * the zone already on the value when written as ISO-8601 strings. - * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATES_WITH_CONTEXT_TIME_ZONE} - * (enabled by default since 2.13) only converts to the mapper TimeZone when - * that timezone was set explicitly. + * Note that {@code ObjectMapper}'s default TimeZone of UTC is implicit, and as such + * does not override the zone already on {@link ZonedDateTime} and {@link OffsetDateTime} + * values written as ISO-8601 Strings: see + * {@link com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializerBase} for details. *

* Granularity of timestamps is controlled through the companion features * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} and diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java index 2f986ff5..c33706f9 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/InstantSerializerBase.java @@ -43,14 +43,23 @@ * Base class for serializers used for {@link java.time.Instant} and * other {@link Temporal} subtypes. *

- * When writing ISO-8601 strings, the mapper {@link java.util.TimeZone} is - * applied only if it was set explicitly - * ({@code SerializationConfig.hasExplicitTimeZone()} is true) and - * {@link com.fasterxml.jackson.databind.SerializationFeature#WRITE_DATES_WITH_CONTEXT_TIME_ZONE} - * is enabled (the default since 2.13). {@code ObjectMapper}'s documented - * default of UTC is implicit until {@code setTimeZone} or - * {@code ObjectWriter.with(TimeZone)} is called, so {@link java.time.ZonedDateTime} - * keeps the zone already on the value. + * When writing ISO-8601 Strings, the context {@link java.util.TimeZone} is applied + * only if all of the following hold: + *

+ * Otherwise the zone (or offset) already on the value being written is retained as-is. */ @SuppressWarnings("serial") public abstract class InstantSerializerBase