From e010dfbf292c08d856344cad9267449094581af5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 17 Nov 2025 14:41:45 -0800 Subject: [PATCH 1/4] Fix #376: add factory methods/constructor for OffsetDateTime ser/deser with custom formatter --- .../jsr310/deser/InstantDeserializer.java | 16 +++ .../jsr310/ser/OffsetDateTimeSerializer.java | 13 ++ .../jsr310/deser/OffsetDateTimeDeserTest.java | 93 ++++++++++++++ .../jsr310/ser/OffsetDateTimeSerTest.java | 120 ++++++++++++++++++ 4 files changed, 242 insertions(+) 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 c882d541..c41c2a12 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 @@ -299,6 +299,22 @@ protected InstantDeserializer(InstantDeserializer base, _alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS); } + /** + * Factory method to create a new deserializer instance with a custom {@link DateTimeFormatter}. + * This is primarily intended for {@link OffsetDateTime} and {@link ZonedDateTime} deserialization, + * allowing customization of parsing behavior (e.g., defaulting offset values or controlling nano-second precision). + * + * @param base Base deserializer to copy settings from (typically one of the static instances like + * {@link #OFFSET_DATE_TIME}, {@link #ZONED_DATE_TIME}, or {@link #INSTANT}) + * @param formatter Custom {@link DateTimeFormatter} to use for parsing + * @return New deserializer instance with the custom formatter + * @since 2.19 + */ + public static InstantDeserializer withCustomFormatter( + InstantDeserializer base, DateTimeFormatter formatter) { + return new InstantDeserializer<>(base, formatter); + } + @Override protected InstantDeserializer withDateFormat(DateTimeFormatter dtf) { if (dtf == _formatter) { diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java index ff7bd4c7..8abfb89d 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java @@ -35,6 +35,19 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest super(base, useTimestamp, base._useNanoseconds, formatter, shape); } + /** + * Constructor for creating a new serializer instance with a custom {@link DateTimeFormatter}. + * This allows customization of the serialization output format, such as controlling nano-second precision + * (e.g., 3 digits instead of 9). + * + * @param formatter Custom {@link DateTimeFormatter} to use for formatting + * @since 2.19 + */ + public OffsetDateTimeSerializer(DateTimeFormatter formatter) { + // Call the protected constructor with useTimestamp=false to ensure string serialization + this(INSTANCE, false, formatter, JsonFormat.Shape.STRING); + } + @Override protected JSR310FormattedSerializerBase withFormat(Boolean useTimestamp, DateTimeFormatter formatter, JsonFormat.Shape shape) diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java index 26b7423b..470df824 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; @@ -873,4 +874,96 @@ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone) private static String offsetWithoutColon(String string){ return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString(); } + + /* + /********************************************************** + /* Tests for custom formatter (#376) + /********************************************************** + */ + + @Test + public void testDeserializationWithCustomFormatter() throws Exception + { + // Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0 + DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .optionalStart() + .parseLenient() + .appendOffsetId() + .parseStrict() + .optionalEnd() + .parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0) + .toFormatter(); + + // Create custom deserializer with the custom formatter + InstantDeserializer customDeserializer = + InstantDeserializer.withCustomFormatter(InstantDeserializer.OFFSET_DATE_TIME, customFormatter); + + // Create a custom module to override the default deserializer + com.fasterxml.jackson.databind.module.SimpleModule customModule = + new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); + customModule.addDeserializer(OffsetDateTime.class, customDeserializer); + + // Add both JavaTimeModule (for other types) and our custom module + // The custom module will override OffsetDateTime deserialization + ObjectMapper mapper = mapperBuilder() + .addModule(customModule) + .build(); + + // Test deserializing date-time without offset (should default to +00:00) + // This is the main use case from issue #376 - parsing ISO_LOCAL_DATE_TIME with default offset + String jsonWithoutOffset = q("2025-01-01T22:01:05"); + OffsetDateTime result = mapper.readValue(jsonWithoutOffset, OffsetDateTime.class); + + assertNotNull(result); + assertEquals(2025, result.getYear()); + assertEquals(1, result.getMonthValue()); + assertEquals(1, result.getDayOfMonth()); + assertEquals(22, result.getHour()); + assertEquals(1, result.getMinute()); + assertEquals(5, result.getSecond()); + assertEquals(ZoneOffset.UTC, result.getOffset()); + + // Test that standard ISO format with offset still works + String jsonWithOffset = q("2025-01-01T22:01:05+02:00"); + OffsetDateTime resultWithOffset = mapper.readValue(jsonWithOffset, OffsetDateTime.class); + + assertNotNull(resultWithOffset); + assertEquals(2025, resultWithOffset.getYear()); + // Verify parsing succeeded - the exact time may be adjusted based on offset conversion + assertTrue(resultWithOffset.toInstant().equals(OffsetDateTime.parse("2025-01-01T22:01:05+02:00").toInstant())); + } + + @Test + public void testDeserializationWithCustomFormatterRoundTrip() throws Exception + { + // Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0 + DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .optionalStart() + .parseLenient() + .appendOffsetId() + .parseStrict() + .optionalEnd() + .parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0) + .toFormatter(); + + InstantDeserializer customDeserializer = + InstantDeserializer.withCustomFormatter(InstantDeserializer.OFFSET_DATE_TIME, customFormatter); + + com.fasterxml.jackson.databind.module.SimpleModule customModule = + new com.fasterxml.jackson.databind.module.SimpleModule(); + customModule.addDeserializer(OffsetDateTime.class, customDeserializer); + + ObjectMapper mapper = mapperBuilder() + .addModule(customModule) + .build(); + + // Verify standard ISO format still works + OffsetDateTime original = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 0, ZoneOffset.UTC); + String json = mapper.writeValueAsString(original); + OffsetDateTime roundTripped = mapper.readValue(json, OffsetDateTime.class); + + assertIsEqual(original, roundTripped); + } } diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java index e41dcd12..d950c25f 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java @@ -3,6 +3,7 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.Temporal; @@ -13,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; @@ -284,4 +286,122 @@ public void testShapeInt() throws Exception { String json1 = newMapper().writeValueAsString(new Pojo1()); assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1); } + + /* + /********************************************************** + /* Tests for custom formatter (#376) + /********************************************************** + */ + + @Test + public void testSerializationWithCustomFormatter() throws Exception + { + // Create a custom formatter that displays only 3 digits of nano-seconds instead of 9 + // Use ISO_LOCAL_DATE and ISO_LOCAL_TIME separately to control nanosecond precision + DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2) + .optionalStart() + .appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true) + .optionalEnd() + .optionalEnd() + .appendOffsetId() + .toFormatter(); + + OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); + + com.fasterxml.jackson.databind.module.SimpleModule customModule = + new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); + customModule.addSerializer(OffsetDateTime.class, customSerializer); + + // Add both JavaTimeModule and our custom module + ObjectMapper mapper = mapperBuilder() + .addModule(customModule) + .build(); + + // Create a date with nanoseconds (123456789 nanos = .123456789 seconds) + OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC); + String json = mapper.writeValueAsString(date); + + // Should output with only 3 digits of nano precision (.123 instead of .123456789) + assertEquals(q("2025-01-01T22:01:05.123Z"), json); + } + + @Test + public void testSerializationWithCustomFormatterNoNanos() throws Exception + { + // Create a formatter without nanoseconds + DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2) + .optionalEnd() + .appendOffsetId() + .toFormatter(); + + OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); + + com.fasterxml.jackson.databind.module.SimpleModule customModule = + new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); + customModule.addSerializer(OffsetDateTime.class, customSerializer); + + ObjectMapper mapper = mapperBuilder() + .addModule(customModule) + .build(); + + OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC); + String json = mapper.writeValueAsString(date); + + // Should output without nanoseconds + assertEquals(q("2025-01-01T22:01:05Z"), json); + } + + @Test + public void testSerializationWithCustomFormatterAndOffset() throws Exception + { + // Create a custom formatter that displays only 3 digits of nano-seconds + DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2) + .optionalStart() + .appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true) + .optionalEnd() + .optionalEnd() + .appendOffsetId() + .toFormatter(); + + OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); + + com.fasterxml.jackson.databind.module.SimpleModule customModule = + new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); + customModule.addSerializer(OffsetDateTime.class, customSerializer); + + ObjectMapper mapper = mapperBuilder() + .addModule(customModule) + .build(); + + // Create a date with a non-UTC offset + OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.ofHours(5)); + String json = mapper.writeValueAsString(date); + + // Should output with offset +05:00 and 3 digits of nano precision + assertEquals(q("2025-01-01T22:01:05.123+05:00"), json); + } } From 41753dd9115fc393be7ee5e0c09931060e800476 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 17 Nov 2025 14:57:34 -0800 Subject: [PATCH 2/4] Rewrite serializer side --- .../jsr310/ser/OffsetDateTimeSerializer.java | 13 ++--- .../jsr310/ser/OffsetDateTimeSerTest.java | 55 ++++++------------- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java index 8abfb89d..a07c2d01 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java @@ -36,16 +36,11 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest } /** - * Constructor for creating a new serializer instance with a custom {@link DateTimeFormatter}. - * This allows customization of the serialization output format, such as controlling nano-second precision - * (e.g., 3 digits instead of 9). - * - * @param formatter Custom {@link DateTimeFormatter} to use for formatting - * @since 2.19 + * @since 2.21 */ - public OffsetDateTimeSerializer(DateTimeFormatter formatter) { - // Call the protected constructor with useTimestamp=false to ensure string serialization - this(INSTANCE, false, formatter, JsonFormat.Shape.STRING); + public OffsetDateTimeSerializer withFormatter(DateTimeFormatter formatter) + { + return new OffsetDateTimeSerializer(this, _useTimestamp, formatter, _shape); } @Override diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java index d950c25f..0fd0cf3c 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java @@ -6,6 +6,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.Temporal; import java.util.TimeZone; @@ -14,7 +15,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; @@ -298,7 +299,7 @@ public void testSerializationWithCustomFormatter() throws Exception { // Create a custom formatter that displays only 3 digits of nano-seconds instead of 9 // Use ISO_LOCAL_DATE and ISO_LOCAL_TIME separately to control nanosecond precision - DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + DateTimeFormatter customFormatter = new DateTimeFormatterBuilder() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) @@ -314,20 +315,9 @@ public void testSerializationWithCustomFormatter() throws Exception .appendOffsetId() .toFormatter(); - OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); - - com.fasterxml.jackson.databind.module.SimpleModule customModule = - new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); - customModule.addSerializer(OffsetDateTime.class, customSerializer); - - // Add both JavaTimeModule and our custom module - ObjectMapper mapper = mapperBuilder() - .addModule(customModule) - .build(); - // Create a date with nanoseconds (123456789 nanos = .123456789 seconds) OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC); - String json = mapper.writeValueAsString(date); + String json = _mapper(customFormatter).writeValueAsString(date); // Should output with only 3 digits of nano precision (.123 instead of .123456789) assertEquals(q("2025-01-01T22:01:05.123Z"), json); @@ -337,7 +327,7 @@ public void testSerializationWithCustomFormatter() throws Exception public void testSerializationWithCustomFormatterNoNanos() throws Exception { // Create a formatter without nanoseconds - DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + DateTimeFormatter customFormatter = new DateTimeFormatterBuilder() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) @@ -350,18 +340,8 @@ public void testSerializationWithCustomFormatterNoNanos() throws Exception .appendOffsetId() .toFormatter(); - OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); - - com.fasterxml.jackson.databind.module.SimpleModule customModule = - new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); - customModule.addSerializer(OffsetDateTime.class, customSerializer); - - ObjectMapper mapper = mapperBuilder() - .addModule(customModule) - .build(); - OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC); - String json = mapper.writeValueAsString(date); + String json = _mapper(customFormatter).writeValueAsString(date); // Should output without nanoseconds assertEquals(q("2025-01-01T22:01:05Z"), json); @@ -371,7 +351,7 @@ public void testSerializationWithCustomFormatterNoNanos() throws Exception public void testSerializationWithCustomFormatterAndOffset() throws Exception { // Create a custom formatter that displays only 3 digits of nano-seconds - DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder() + DateTimeFormatter customFormatter = new DateTimeFormatterBuilder() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2) @@ -387,21 +367,22 @@ public void testSerializationWithCustomFormatterAndOffset() throws Exception .appendOffsetId() .toFormatter(); - OffsetDateTimeSerializer customSerializer = new OffsetDateTimeSerializer(customFormatter); - - com.fasterxml.jackson.databind.module.SimpleModule customModule = - new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); - customModule.addSerializer(OffsetDateTime.class, customSerializer); - - ObjectMapper mapper = mapperBuilder() - .addModule(customModule) - .build(); // Create a date with a non-UTC offset OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.ofHours(5)); - String json = mapper.writeValueAsString(date); + String json = _mapper(customFormatter).writeValueAsString(date); // Should output with offset +05:00 and 3 digits of nano precision assertEquals(q("2025-01-01T22:01:05.123+05:00"), json); } + + private ObjectMapper _mapper(DateTimeFormatter dtf) { + OffsetDateTimeSerializer customSerializer = OffsetDateTimeSerializer.INSTANCE + .withFormatter(dtf); + SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule") + .addSerializer(OffsetDateTime.class, customSerializer); + return mapperBuilder() + .addModule(customModule) + .build(); + } } From 53eed8ea8dd15c503d875bc0e7290c40359dbb4b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 17 Nov 2025 15:06:34 -0800 Subject: [PATCH 3/4] Refactor deser side too --- .../jsr310/deser/InstantDeserializer.java | 29 +++++------------- .../jsr310/ser/OffsetDateTimeSerializer.java | 14 +++++++++ .../jsr310/deser/OffsetDateTimeDeserTest.java | 30 ++++++++----------- 3 files changed, 35 insertions(+), 38 deletions(-) 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 c41c2a12..1a5e805f 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 @@ -200,12 +200,12 @@ protected InstantDeserializer(Class supportedType, */ @Deprecated() protected InstantDeserializer(Class supportedType, - DateTimeFormatter formatter, - Function parsedToValue, - Function fromMilliseconds, - Function fromNanoseconds, - BiFunction adjust, - boolean replaceZeroOffsetAsZ + DateTimeFormatter formatter, + Function parsedToValue, + Function fromMilliseconds, + Function fromNanoseconds, + BiFunction adjust, + boolean replaceZeroOffsetAsZ ) { this(supportedType, formatter, parsedToValue, fromMilliseconds, fromNanoseconds, adjust, replaceZeroOffsetAsZ, @@ -300,23 +300,10 @@ protected InstantDeserializer(InstantDeserializer base, } /** - * Factory method to create a new deserializer instance with a custom {@link DateTimeFormatter}. - * This is primarily intended for {@link OffsetDateTime} and {@link ZonedDateTime} deserialization, - * allowing customization of parsing behavior (e.g., defaulting offset values or controlling nano-second precision). - * - * @param base Base deserializer to copy settings from (typically one of the static instances like - * {@link #OFFSET_DATE_TIME}, {@link #ZONED_DATE_TIME}, or {@link #INSTANT}) - * @param formatter Custom {@link DateTimeFormatter} to use for parsing - * @return New deserializer instance with the custom formatter - * @since 2.19 + * NOTE: {@code public} since 2.21 */ - public static InstantDeserializer withCustomFormatter( - InstantDeserializer base, DateTimeFormatter formatter) { - return new InstantDeserializer<>(base, formatter); - } - @Override - protected InstantDeserializer withDateFormat(DateTimeFormatter dtf) { + public InstantDeserializer withDateFormat(DateTimeFormatter dtf) { if (dtf == _formatter) { return this; } diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java index a07c2d01..1a961c8d 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java @@ -36,6 +36,20 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest } /** + * Method for constructing a new {@code OffsetDateTimeSerializer} with settings + * of this serializer but with custom {@link DateTimeFormatter} overrides. + * Commonly used on {@code INSTANCE} like so: + *
+     *  DateTimeFormatter dtf = new DateTimeFormatterBuilder()
+     *          .append(DateTimeFormatter.ISO_LOCAL_DATE)
+     *          .appendLiteral('T')
+     *          // and so on
+     *          .toFormatter();
+     *  OffsetDateTimeSerializer ser = OffsetDateTimeSerializer.INSTANCE
+     *          .withFormatter(dtf);
+     *  // register via Module
+     *
+ * * @since 2.21 */ public OffsetDateTimeSerializer withFormatter(DateTimeFormatter formatter) diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java index 470df824..9c21a4e5 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java @@ -13,12 +13,11 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Feature; + import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; @@ -876,9 +875,9 @@ private static String offsetWithoutColon(String string){ } /* - /********************************************************** - /* Tests for custom formatter (#376) - /********************************************************** + /********************************************************************** + /* Tests for custom formatter (modules-java8#376) + /********************************************************************** */ @Test @@ -897,12 +896,11 @@ public void testDeserializationWithCustomFormatter() throws Exception // Create custom deserializer with the custom formatter InstantDeserializer customDeserializer = - InstantDeserializer.withCustomFormatter(InstantDeserializer.OFFSET_DATE_TIME, customFormatter); + InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter); // Create a custom module to override the default deserializer - com.fasterxml.jackson.databind.module.SimpleModule customModule = - new com.fasterxml.jackson.databind.module.SimpleModule("CustomOffsetDateTimeModule"); - customModule.addDeserializer(OffsetDateTime.class, customDeserializer); + SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule") + .addDeserializer(OffsetDateTime.class, customDeserializer); // Add both JavaTimeModule (for other types) and our custom module // The custom module will override OffsetDateTime deserialization @@ -949,11 +947,9 @@ public void testDeserializationWithCustomFormatterRoundTrip() throws Exception .toFormatter(); InstantDeserializer customDeserializer = - InstantDeserializer.withCustomFormatter(InstantDeserializer.OFFSET_DATE_TIME, customFormatter); - - com.fasterxml.jackson.databind.module.SimpleModule customModule = - new com.fasterxml.jackson.databind.module.SimpleModule(); - customModule.addDeserializer(OffsetDateTime.class, customDeserializer); + InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter); + SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule") + .addDeserializer(OffsetDateTime.class, customDeserializer); ObjectMapper mapper = mapperBuilder() .addModule(customModule) From 739f8ec8be611ae34a083bf35180717722a6a53d Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 17 Nov 2025 15:10:50 -0800 Subject: [PATCH 4/4] Add release notes --- .../jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java | 6 +++--- release-notes/VERSION-2.x | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java index 0fd0cf3c..689d8644 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java @@ -289,9 +289,9 @@ public void testShapeInt() throws Exception { } /* - /********************************************************** - /* Tests for custom formatter (#376) - /********************************************************** + /********************************************************************** + /* Tests for custom formatter (modules-java8#376) + /********************************************************************** */ @Test diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index bdd6942c..63ec26b5 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -14,6 +14,9 @@ Modules: negative timestamps incorrectly (reported by Kevin M) (fix by @cowtowncoder, w/ Claude code) +#376: Allow specifying custom `DateTimeFormatter` for `OffsetDateTime` ser/deser + (new constructors?) + (requested by @ZIRAKrezovic) No changes since 2.20