From 360a960ba6240cbda14de0f1e71f4d88248c34bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stan=20Bron=C3=A9?= Date: Tue, 5 Aug 2025 13:48:18 +0200 Subject: [PATCH 1/6] Added support for OffsetDateTime and ZonedDateTime. Changed LocalDateTimeValueReader not to crash when a string was fed that accidentally included a timezone. --- .../javatime/DefaultDateTimeValueReader.java | 27 ++++++ .../javatime/JacksonJrJavaTimeExtension.java | 6 +- .../JavaTimeReaderWriterProvider.java | 70 +++++++++----- .../javatime/LocalDateTimeValueReader.java | 28 ++++-- .../javatime/LocalDateTimeValueWriter.java | 16 +--- .../javatime/OffsetDateTimeValueWriter.java | 22 +++++ .../javatime/ZonedDateTimeValueWriter.java | 22 +++++ .../javatime/LocalDateTimeReaderTest.java | 95 +++++++++++++++++++ 8 files changed, 239 insertions(+), 47 deletions(-) create mode 100644 jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java create mode 100644 jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java create mode 100644 jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java create mode 100644 jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java new file mode 100644 index 00000000..fc5fe347 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQuery; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.jr.ob.api.ValueReader; +import com.fasterxml.jackson.jr.ob.impl.JSONReader; + +public class DefaultDateTimeValueReader extends ValueReader { + + private final TemporalQuery _query; + + public DefaultDateTimeValueReader(Class targetType, TemporalQuery query) { + super(targetType); + + this._query = Objects.requireNonNull(query); + } + + @Override + public Object read(JSONReader reader, JsonParser p) throws IOException { + return JavaTimeReaderWriterProvider.FORMATTER.parse(p.getText(), _query); + } + +} \ No newline at end of file diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JacksonJrJavaTimeExtension.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JacksonJrJavaTimeExtension.java index a1077d78..0e16d8c2 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JacksonJrJavaTimeExtension.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JacksonJrJavaTimeExtension.java @@ -6,15 +6,15 @@ public class JacksonJrJavaTimeExtension extends JacksonJrExtension { final static JavaTimeReaderWriterProvider DEFAULT_RW_PROVIDER = new JavaTimeReaderWriterProvider(); - private JavaTimeReaderWriterProvider readerWriterProvider = DEFAULT_RW_PROVIDER; + private JavaTimeReaderWriterProvider _readerWriterProvider = DEFAULT_RW_PROVIDER; @Override protected void register(ExtensionContext ctxt) { - ctxt.insertProvider(readerWriterProvider); + ctxt.insertProvider(_readerWriterProvider); } public JacksonJrJavaTimeExtension with(JavaTimeReaderWriterProvider p) { - readerWriterProvider = p; + _readerWriterProvider = p; return this; } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java index bf204791..19741961 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java @@ -1,48 +1,70 @@ package com.fasterxml.jackson.jr.extension.javatime; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + import com.fasterxml.jackson.jr.ob.api.ReaderWriterProvider; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.api.ValueWriter; import com.fasterxml.jackson.jr.ob.impl.JSONReader; import com.fasterxml.jackson.jr.ob.impl.JSONWriter; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - /** * Provider for {@link ValueReader}s and {@link ValueWriter}s for Date/Time * types supported by Java Time Extension. */ public class JavaTimeReaderWriterProvider extends ReaderWriterProvider { - private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; - + protected static final DateTimeFormatter FORMATTER; + + static { + FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 9, true) + .optionalEnd() + .optionalStart() + .appendOffsetId() + .optionalEnd() + .optionalStart() + .appendLiteral('[') + .appendZoneRegionId() + .appendLiteral(']') + .optionalEnd() + .toFormatter(); + } + public JavaTimeReaderWriterProvider() { } @Override public ValueReader findValueReader(JSONReader readContext, Class type) { - return LocalDateTime.class.isAssignableFrom(type) ? new LocalDateTimeValueReader(dateTimeFormatter) : null; + if (LocalDateTime.class.isAssignableFrom(type)) { + return new LocalDateTimeValueReader(); + } + if (OffsetDateTime.class.isAssignableFrom(type)) { + return new DefaultDateTimeValueReader(OffsetDateTime.class, OffsetDateTime::from); + } + if (ZonedDateTime.class.isAssignableFrom(type)) { + return new DefaultDateTimeValueReader(ZonedDateTime.class, ZonedDateTime::from); + } + return null; } @Override public ValueWriter findValueWriter(JSONWriter writeContext, Class type) { - return LocalDateTime.class.isAssignableFrom(type) ? new LocalDateTimeValueWriter(dateTimeFormatter) : null; - } - - /** - * Method for reconfiguring {@link DateTimeFormatter} used for reading/writing - * following Date/Time value types: - *
    - *
  • {@code java.time.LocalDateTime} - *
  • - *
- * - * @param formatter - * - * @return This provider instance for call chaining - */ - public JavaTimeReaderWriterProvider withDateTimeFormatter(DateTimeFormatter formatter) { - dateTimeFormatter = formatter; - return this; + if (LocalDateTime.class.isAssignableFrom(type)) { + return new LocalDateTimeValueWriter(); + } + if (OffsetDateTime.class.isAssignableFrom(type)) { + return new OffsetDateTimeValueWriter(); + } + if (ZonedDateTime.class.isAssignableFrom(type)) { + return new ZonedDateTimeValueWriter(); + } + return null; } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java index cc133327..ec4daa92 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java @@ -1,23 +1,33 @@ package com.fasterxml.jackson.jr.extension.javatime; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - public class LocalDateTimeValueReader extends ValueReader { - private final DateTimeFormatter formatter; - - public LocalDateTimeValueReader(DateTimeFormatter formatter) { + public LocalDateTimeValueReader() { super(LocalDateTime.class); - this.formatter = formatter; } @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - return LocalDateTime.parse(p.getText(), formatter); + final TemporalAccessor ta = JavaTimeReaderWriterProvider.FORMATTER.parseBest(p.getText(), + ZonedDateTime::from, LocalDateTime::from); + + if (ta instanceof ZonedDateTime) { + // Convert a date time that unexpectedly includes a time offset or zone ID, to a proper local date time + return ((ZonedDateTime)ta).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); + } + if (ta instanceof LocalDateTime) { + return ta; + } + + throw new IOException("Could not create a valid DateTime instance"); } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java index 020963f6..bf9c5eab 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java @@ -1,23 +1,17 @@ package com.fasterxml.jackson.jr.extension.javatime; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.jr.ob.api.ValueWriter; -import com.fasterxml.jackson.jr.ob.impl.JSONWriter; - import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -public class LocalDateTimeValueWriter implements ValueWriter { - private final DateTimeFormatter formatter; - - public LocalDateTimeValueWriter(DateTimeFormatter formatter) { - this.formatter = formatter; - } +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.jr.ob.api.ValueWriter; +import com.fasterxml.jackson.jr.ob.impl.JSONWriter; +public class LocalDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { - String localDateTimeString = ((LocalDateTime) value).format(formatter); + String localDateTimeString = ((LocalDateTime) value).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); context.writeValue(localDateTimeString); } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java new file mode 100644 index 00000000..1cc6d455 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java @@ -0,0 +1,22 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.jr.ob.api.ValueWriter; +import com.fasterxml.jackson.jr.ob.impl.JSONWriter; + +public class OffsetDateTimeValueWriter implements ValueWriter { + @Override + public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { + String offsetDateTimeString = ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + context.writeValue(offsetDateTimeString); + } + + @Override + public Class valueType() { + return OffsetDateTime.class; + } +} diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java new file mode 100644 index 00000000..cdddb388 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java @@ -0,0 +1,22 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.jr.ob.api.ValueWriter; +import com.fasterxml.jackson.jr.ob.impl.JSONWriter; + +public class ZonedDateTimeValueWriter implements ValueWriter { + @Override + public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { + String zonedDateTimeString = ((ZonedDateTime) value).format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + context.writeValue(zonedDateTimeString); + } + + @Override + public Class valueType() { + return ZonedDateTime.class; + } +} diff --git a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java new file mode 100644 index 00000000..71c1913f --- /dev/null +++ b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java @@ -0,0 +1,95 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.jr.ob.JSON; +import com.fasterxml.jackson.jr.ob.JSONObjectException; + +public class LocalDateTimeReaderTest { + + private static JSON DATETIME_JSON = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); + + @Test + public void testRead() throws JSONObjectException, IOException { + final LocalDateTime local = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15.123456789\""); + + Assertions.assertEquals(2025, local.getYear()); + Assertions.assertEquals(8, local.getMonthValue()); + Assertions.assertEquals(4, local.getDayOfMonth()); + Assertions.assertEquals(14, local.getHour()); + Assertions.assertEquals(34, local.getMinute()); + Assertions.assertEquals(15, local.getSecond()); + Assertions.assertEquals(123, local.getLong(ChronoField.MILLI_OF_SECOND)); + + final LocalDateTime localWithoutMs = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15\""); + + Assertions.assertEquals(2025, localWithoutMs.getYear()); + Assertions.assertEquals(8, localWithoutMs.getMonthValue()); + Assertions.assertEquals(4, localWithoutMs.getDayOfMonth()); + Assertions.assertEquals(14, localWithoutMs.getHour()); + Assertions.assertEquals(34, localWithoutMs.getMinute()); + Assertions.assertEquals(15, localWithoutMs.getSecond()); + Assertions.assertEquals(0, localWithoutMs.getLong(ChronoField.MILLI_OF_SECOND)); + + final LocalDateTime localWithTz = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T12:34:15.123Z\""); + + Assertions.assertEquals(2025, localWithTz.getYear()); + Assertions.assertEquals(8, localWithTz.getMonthValue()); + Assertions.assertEquals(4, localWithTz.getDayOfMonth()); + Assertions.assertEquals(14, localWithTz.getHour()); + Assertions.assertEquals(34, localWithTz.getMinute()); + Assertions.assertEquals(15, localWithTz.getSecond()); + Assertions.assertEquals(123, localWithTz.getLong(ChronoField.MILLI_OF_SECOND)); + + final OffsetDateTime offset = DATETIME_JSON.beanFrom(OffsetDateTime.class, "\"2025-08-04T12:34:15.123+02:00\""); + + Assertions.assertEquals(2025, offset.getYear()); + Assertions.assertEquals(8, offset.getMonthValue()); + Assertions.assertEquals(4, offset.getDayOfMonth()); + Assertions.assertEquals(12, offset.getHour()); + Assertions.assertEquals(34, offset.getMinute()); + Assertions.assertEquals(15, offset.getSecond()); + Assertions.assertEquals(123, offset.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, offset.getOffset().getTotalSeconds()); + + final ZonedDateTime zoned = DATETIME_JSON.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\""); + + Assertions.assertEquals(2025, zoned.getYear()); + Assertions.assertEquals(8, zoned.getMonthValue()); + Assertions.assertEquals(4, zoned.getDayOfMonth()); + Assertions.assertEquals(12, zoned.getHour()); + Assertions.assertEquals(34, zoned.getMinute()); + Assertions.assertEquals(15, zoned.getSecond()); + Assertions.assertEquals(123, zoned.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, zoned.getOffset().getTotalSeconds()); + Assertions.assertEquals("Europe/Berlin", zoned.getZone().getId()); + } + + @Test + public void testWrite() throws JSONObjectException, IOException { + final LocalDateTime ldt = LocalDateTime.of(2025, 8, 4, 14, 34, 15, 123000000); + final String ldtString = DATETIME_JSON.composeString().addObject(ldt).finish(); + + Assertions.assertEquals("\"2025-08-04T14:34:15.123\"", ldtString); + + final OffsetDateTime odt = OffsetDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneOffset.ofHours(2)); + final String odtString = DATETIME_JSON.composeString().addObject(odt).finish(); + + Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00\"", odtString); + + final ZonedDateTime zdt = ZonedDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneId.of("Europe/Berlin")); + final String zdtString = DATETIME_JSON.composeString().addObject(zdt).finish(); + + Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\"", zdtString); + } + +} From cc26e74ae742456582396feec5c58daf06cbe465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stan=20Bron=C3=A9?= Date: Wed, 6 Aug 2025 10:32:26 +0200 Subject: [PATCH 2/6] Fixed unit test issue. Also changed the parsing of offset dates to be slightly more forgiving. --- .../JavaTimeReaderWriterProvider.java | 74 +++++-- .../javatime/LocalDateTimeValueReader.java | 10 +- .../javatime/LocalDateTimeReaderTest.java | 199 +++++++++++------- 3 files changed, 184 insertions(+), 99 deletions(-) diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java index 19741961..f350b46d 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -19,31 +20,19 @@ */ public class JavaTimeReaderWriterProvider extends ReaderWriterProvider { - protected static final DateTimeFormatter FORMATTER; - - static { - FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd'T'HH:mm:ss") - .optionalStart() - .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 9, true) - .optionalEnd() - .optionalStart() - .appendOffsetId() - .optionalEnd() - .optionalStart() - .appendLiteral('[') - .appendZoneRegionId() - .appendLiteral(']') - .optionalEnd() - .toFormatter(); - } + private ZoneId _fallbackLocalZoneId; + + protected static final DateTimeFormatter FORMATTER = createFormatter(true); + protected static final DateTimeFormatter LOCAL_FORMATTER = createFormatter(false); - public JavaTimeReaderWriterProvider() { } + public JavaTimeReaderWriterProvider() { + _fallbackLocalZoneId = ZoneId.systemDefault(); + } @Override public ValueReader findValueReader(JSONReader readContext, Class type) { if (LocalDateTime.class.isAssignableFrom(type)) { - return new LocalDateTimeValueReader(); + return new LocalDateTimeValueReader(_fallbackLocalZoneId); } if (OffsetDateTime.class.isAssignableFrom(type)) { return new DefaultDateTimeValueReader(OffsetDateTime.class, OffsetDateTime::from); @@ -67,4 +56,49 @@ public ValueWriter findValueWriter(JSONWriter writeContext, Class type) { } return null; } + + /** + * Setter to configure a time zone that is to be applied when a zoned ISO 8601 date time needs + * to be converted to a LocalDateTime. Can be set to null to apply + * the system default. + * @see java.time.LocalDateTime + * @param fallbackLocalZoneId Time zone to apply, or null + * @return Reference for chaining + */ + public JavaTimeReaderWriterProvider setLocalFallbackTimeZone(ZoneId fallbackLocalZoneId) { + _fallbackLocalZoneId = fallbackLocalZoneId == null ? ZoneId.systemDefault() : fallbackLocalZoneId; + return this; + } + + /** + * Create a forgiving date time formatter that allows different interpretations of ISO 8601 + * strings to be parsed. + * + * @param includeUtcDefault Set to true to set UTC to be the default offset + * for non-local date times. Set to false when handling + * local date times. + * @return Formatter + */ + public static DateTimeFormatter createFormatter(boolean includeUtcDefault) { + final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() + .parseLenient() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.MILLI_OF_SECOND, 1, 9, true) + .optionalEnd() + .optionalStart() + .appendOffsetId() + .optionalStart() + .appendLiteral('[') + .appendZoneRegionId() + .appendLiteral(']') + .optionalEnd() + .optionalEnd(); + + if (includeUtcDefault) { + builder.parseDefaulting(ChronoField.OFFSET_SECONDS, 0); + } + + return builder.toFormatter(); + } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java index ec4daa92..b1430198 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java @@ -5,24 +5,28 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; +import java.util.Objects; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; public class LocalDateTimeValueReader extends ValueReader { - public LocalDateTimeValueReader() { + private final ZoneId _localZoneId; + + public LocalDateTimeValueReader(ZoneId localZoneId) { super(LocalDateTime.class); + _localZoneId = Objects.requireNonNull(localZoneId); } @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - final TemporalAccessor ta = JavaTimeReaderWriterProvider.FORMATTER.parseBest(p.getText(), + final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(), ZonedDateTime::from, LocalDateTime::from); if (ta instanceof ZonedDateTime) { // Convert a date time that unexpectedly includes a time offset or zone ID, to a proper local date time - return ((ZonedDateTime)ta).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); + return ((ZonedDateTime)ta).withZoneSameInstant(_localZoneId).toLocalDateTime(); } if (ta instanceof LocalDateTime) { return ta; diff --git a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java index 71c1913f..788f4c0d 100644 --- a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java +++ b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java @@ -16,80 +16,127 @@ public class LocalDateTimeReaderTest { - private static JSON DATETIME_JSON = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); - - @Test - public void testRead() throws JSONObjectException, IOException { - final LocalDateTime local = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15.123456789\""); - - Assertions.assertEquals(2025, local.getYear()); - Assertions.assertEquals(8, local.getMonthValue()); - Assertions.assertEquals(4, local.getDayOfMonth()); - Assertions.assertEquals(14, local.getHour()); - Assertions.assertEquals(34, local.getMinute()); - Assertions.assertEquals(15, local.getSecond()); - Assertions.assertEquals(123, local.getLong(ChronoField.MILLI_OF_SECOND)); - - final LocalDateTime localWithoutMs = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15\""); - - Assertions.assertEquals(2025, localWithoutMs.getYear()); - Assertions.assertEquals(8, localWithoutMs.getMonthValue()); - Assertions.assertEquals(4, localWithoutMs.getDayOfMonth()); - Assertions.assertEquals(14, localWithoutMs.getHour()); - Assertions.assertEquals(34, localWithoutMs.getMinute()); - Assertions.assertEquals(15, localWithoutMs.getSecond()); - Assertions.assertEquals(0, localWithoutMs.getLong(ChronoField.MILLI_OF_SECOND)); - - final LocalDateTime localWithTz = DATETIME_JSON.beanFrom(LocalDateTime.class, "\"2025-08-04T12:34:15.123Z\""); - - Assertions.assertEquals(2025, localWithTz.getYear()); - Assertions.assertEquals(8, localWithTz.getMonthValue()); - Assertions.assertEquals(4, localWithTz.getDayOfMonth()); - Assertions.assertEquals(14, localWithTz.getHour()); - Assertions.assertEquals(34, localWithTz.getMinute()); - Assertions.assertEquals(15, localWithTz.getSecond()); - Assertions.assertEquals(123, localWithTz.getLong(ChronoField.MILLI_OF_SECOND)); - - final OffsetDateTime offset = DATETIME_JSON.beanFrom(OffsetDateTime.class, "\"2025-08-04T12:34:15.123+02:00\""); - - Assertions.assertEquals(2025, offset.getYear()); - Assertions.assertEquals(8, offset.getMonthValue()); - Assertions.assertEquals(4, offset.getDayOfMonth()); - Assertions.assertEquals(12, offset.getHour()); - Assertions.assertEquals(34, offset.getMinute()); - Assertions.assertEquals(15, offset.getSecond()); - Assertions.assertEquals(123, offset.getLong(ChronoField.MILLI_OF_SECOND)); - Assertions.assertEquals(7200, offset.getOffset().getTotalSeconds()); - - final ZonedDateTime zoned = DATETIME_JSON.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\""); - - Assertions.assertEquals(2025, zoned.getYear()); - Assertions.assertEquals(8, zoned.getMonthValue()); - Assertions.assertEquals(4, zoned.getDayOfMonth()); - Assertions.assertEquals(12, zoned.getHour()); - Assertions.assertEquals(34, zoned.getMinute()); - Assertions.assertEquals(15, zoned.getSecond()); - Assertions.assertEquals(123, zoned.getLong(ChronoField.MILLI_OF_SECOND)); - Assertions.assertEquals(7200, zoned.getOffset().getTotalSeconds()); - Assertions.assertEquals("Europe/Berlin", zoned.getZone().getId()); - } - - @Test - public void testWrite() throws JSONObjectException, IOException { - final LocalDateTime ldt = LocalDateTime.of(2025, 8, 4, 14, 34, 15, 123000000); - final String ldtString = DATETIME_JSON.composeString().addObject(ldt).finish(); - - Assertions.assertEquals("\"2025-08-04T14:34:15.123\"", ldtString); - - final OffsetDateTime odt = OffsetDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneOffset.ofHours(2)); - final String odtString = DATETIME_JSON.composeString().addObject(odt).finish(); - - Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00\"", odtString); - - final ZonedDateTime zdt = ZonedDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneId.of("Europe/Berlin")); - final String zdtString = DATETIME_JSON.composeString().addObject(zdt).finish(); - - Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\"", zdtString); - } - + @Test + public void testRead() throws JSONObjectException, IOException { + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension() + .with(new JavaTimeReaderWriterProvider().setLocalFallbackTimeZone(ZoneId.of("Europe/Berlin")))) + .build(); + + final LocalDateTime local = json.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15.123456789\""); + + Assertions.assertEquals(2025, local.getYear()); + Assertions.assertEquals(8, local.getMonthValue()); + Assertions.assertEquals(4, local.getDayOfMonth()); + Assertions.assertEquals(14, local.getHour()); + Assertions.assertEquals(34, local.getMinute()); + Assertions.assertEquals(15, local.getSecond()); + Assertions.assertEquals(123, local.getLong(ChronoField.MILLI_OF_SECOND)); + + final LocalDateTime localWithoutMs = json.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15\""); + + Assertions.assertEquals(2025, localWithoutMs.getYear()); + Assertions.assertEquals(8, localWithoutMs.getMonthValue()); + Assertions.assertEquals(4, localWithoutMs.getDayOfMonth()); + Assertions.assertEquals(14, localWithoutMs.getHour()); + Assertions.assertEquals(34, localWithoutMs.getMinute()); + Assertions.assertEquals(15, localWithoutMs.getSecond()); + Assertions.assertEquals(0, localWithoutMs.getLong(ChronoField.MILLI_OF_SECOND)); + + final LocalDateTime localWithTz = json.beanFrom(LocalDateTime.class, "\"2025-08-04T12:34:15.123Z\""); + + Assertions.assertEquals(2025, localWithTz.getYear()); + Assertions.assertEquals(8, localWithTz.getMonthValue()); + Assertions.assertEquals(4, localWithTz.getDayOfMonth()); + Assertions.assertEquals(14, localWithTz.getHour()); + Assertions.assertEquals(34, localWithTz.getMinute()); + Assertions.assertEquals(15, localWithTz.getSecond()); + Assertions.assertEquals(123, localWithTz.getLong(ChronoField.MILLI_OF_SECOND)); + + final LocalDateTime localWithTz2 = json.beanFrom(LocalDateTime.class, "\"2025-08-04T20:34:15.123+08:00\""); + + Assertions.assertEquals(2025, localWithTz2.getYear()); + Assertions.assertEquals(8, localWithTz2.getMonthValue()); + Assertions.assertEquals(4, localWithTz2.getDayOfMonth()); + Assertions.assertEquals(14, localWithTz2.getHour()); + Assertions.assertEquals(34, localWithTz2.getMinute()); + Assertions.assertEquals(15, localWithTz2.getSecond()); + Assertions.assertEquals(123, localWithTz2.getLong(ChronoField.MILLI_OF_SECOND)); + + final OffsetDateTime offsetted = json.beanFrom(OffsetDateTime.class, "\"2025-08-04T12:34:15.123+02:00\""); + + Assertions.assertEquals(2025, offsetted.getYear()); + Assertions.assertEquals(8, offsetted.getMonthValue()); + Assertions.assertEquals(4, offsetted.getDayOfMonth()); + Assertions.assertEquals(12, offsetted.getHour()); + Assertions.assertEquals(34, offsetted.getMinute()); + Assertions.assertEquals(15, offsetted.getSecond()); + Assertions.assertEquals(123, offsetted.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, offsetted.getOffset().getTotalSeconds()); + + final OffsetDateTime offsettedtWithoutOffset = json.beanFrom(OffsetDateTime.class, "\"2025-08-04T12:34:15.123\""); + + Assertions.assertEquals(2025, offsettedtWithoutOffset.getYear()); + Assertions.assertEquals(8, offsettedtWithoutOffset.getMonthValue()); + Assertions.assertEquals(4, offsettedtWithoutOffset.getDayOfMonth()); + Assertions.assertEquals(12, offsettedtWithoutOffset.getHour()); + Assertions.assertEquals(34, offsettedtWithoutOffset.getMinute()); + Assertions.assertEquals(15, offsettedtWithoutOffset.getSecond()); + Assertions.assertEquals(123, offsettedtWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(0, offsettedtWithoutOffset.getOffset().getTotalSeconds()); + + final ZonedDateTime zoned = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\""); + + Assertions.assertEquals(2025, zoned.getYear()); + Assertions.assertEquals(8, zoned.getMonthValue()); + Assertions.assertEquals(4, zoned.getDayOfMonth()); + Assertions.assertEquals(12, zoned.getHour()); + Assertions.assertEquals(34, zoned.getMinute()); + Assertions.assertEquals(15, zoned.getSecond()); + Assertions.assertEquals(123, zoned.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, zoned.getOffset().getTotalSeconds()); + Assertions.assertEquals("Europe/Berlin", zoned.getZone().getId()); + + final ZonedDateTime zonedWithoutZoneName = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123+02:00\""); + + Assertions.assertEquals(2025, zonedWithoutZoneName.getYear()); + Assertions.assertEquals(8, zonedWithoutZoneName.getMonthValue()); + Assertions.assertEquals(4, zonedWithoutZoneName.getDayOfMonth()); + Assertions.assertEquals(12, zonedWithoutZoneName.getHour()); + Assertions.assertEquals(34, zonedWithoutZoneName.getMinute()); + Assertions.assertEquals(15, zonedWithoutZoneName.getSecond()); + Assertions.assertEquals(123, zonedWithoutZoneName.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, zonedWithoutZoneName.getOffset().getTotalSeconds()); + + final ZonedDateTime zonedWithoutOffset = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123\""); + + Assertions.assertEquals(2025, zonedWithoutOffset.getYear()); + Assertions.assertEquals(8, zonedWithoutOffset.getMonthValue()); + Assertions.assertEquals(4, zonedWithoutOffset.getDayOfMonth()); + Assertions.assertEquals(12, zonedWithoutOffset.getHour()); + Assertions.assertEquals(34, zonedWithoutOffset.getMinute()); + Assertions.assertEquals(15, zonedWithoutOffset.getSecond()); + Assertions.assertEquals(123, zonedWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(0, zonedWithoutOffset.getOffset().getTotalSeconds()); + } + + @Test + public void testWrite() throws JSONObjectException, IOException { + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); + + final LocalDateTime ldt = LocalDateTime.of(2025, 8, 4, 14, 34, 15, 123000000); + final String ldtString = json.composeString().addObject(ldt).finish(); + + Assertions.assertEquals("\"2025-08-04T14:34:15.123\"", ldtString); + + final OffsetDateTime odt = OffsetDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneOffset.ofHours(2)); + final String odtString = json.composeString().addObject(odt).finish(); + + Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00\"", odtString); + + final ZonedDateTime zdt = ZonedDateTime.of(2025, 8, 4, 12, 34, 15, 123000000, ZoneId.of("Europe/Berlin")); + final String zdtString = json.composeString().addObject(zdt).finish(); + + Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\"", zdtString); + } + } From d117f6dc07fdf76f7ff20dff7e1a38c442fd264b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stan=20Bron=C3=A9?= Date: Tue, 12 Aug 2025 08:47:10 +0200 Subject: [PATCH 3/6] Added Javadoc to classes that were significantly altered. Also included a small change to be kind to null values. --- .../javatime/DefaultDateTimeValueReader.java | 22 ++++++++++++++- .../JavaTimeReaderWriterProvider.java | 2 ++ .../javatime/LocalDateTimeValueReader.java | 27 +++++++++++++++++-- .../javatime/LocalDateTimeValueWriter.java | 7 ++++- .../javatime/OffsetDateTimeValueWriter.java | 6 +++++ .../javatime/ZonedDateTimeValueWriter.java | 6 +++++ .../javatime/LocalDateTimeReaderTest.java | 14 ++++++++++ 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java index fc5fe347..eb7474df 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java @@ -1,18 +1,32 @@ package com.fasterxml.jackson.jr.extension.javatime; import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; import java.util.Objects; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; +/** + * {@link ValueReader} designed to easily handle {@link TemporalAccessor} descendants such as + * {@link OffsetDateTime} and {@link ZonedDateTime}. Their string representation is expected to + * be in ISO 8601 format. + * @since 2.20 + * @see ISO 8601 on Wikipedia + */ public class DefaultDateTimeValueReader extends ValueReader { - private final TemporalQuery _query; + /** + * Constructor that includes a temportal query that is to be used during formatting. + * @param targetType Target type + * @param query Temporal query for parsing + */ public DefaultDateTimeValueReader(Class targetType, TemporalQuery query) { super(targetType); @@ -21,6 +35,12 @@ public DefaultDateTimeValueReader(Class targetType, TemporalQuery query) { @Override public Object read(JSONReader reader, JsonParser p) throws IOException { + // SimpleValueReader allows 'Date' objects to be null, so this should probably + // also be the case here. + if (p.hasToken(JsonToken.VALUE_NULL)) { + return null; + } + return JavaTimeReaderWriterProvider.FORMATTER.parse(p.getText(), _query); } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java index f350b46d..8e40379e 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java @@ -63,6 +63,7 @@ public ValueWriter findValueWriter(JSONWriter writeContext, Class type) { * the system default. * @see java.time.LocalDateTime * @param fallbackLocalZoneId Time zone to apply, or null + * @since 2.20 * @return Reference for chaining */ public JavaTimeReaderWriterProvider setLocalFallbackTimeZone(ZoneId fallbackLocalZoneId) { @@ -77,6 +78,7 @@ public JavaTimeReaderWriterProvider setLocalFallbackTimeZone(ZoneId fallbackLoca * @param includeUtcDefault Set to true to set UTC to be the default offset * for non-local date times. Set to false when handling * local date times. + * @since 2.20 * @return Formatter */ public static DateTimeFormatter createFormatter(boolean includeUtcDefault) { diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java index b1430198..b1b3a381 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java @@ -8,12 +8,26 @@ import java.util.Objects; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; +/** + * {@link ValueReader} designed specifically to handle {@link LocalDateTime} instances. This + * requires a slightly different approach than other date time types because we want to be as + * forgiving as possible and be able to also interpret ISO 8601 dates that include an offset + * or zone ID. + * @since 2.20 + * @see ISO 8601 on Wikipedia + */ public class LocalDateTimeValueReader extends ValueReader { private final ZoneId _localZoneId; + /** + * Constructor that accepts a zone ID that should be used to when a ISO 8601 string that + * includes an offset needs to be converted to a local date time. + * @param localZoneId Destination zone ID + */ public LocalDateTimeValueReader(ZoneId localZoneId) { super(LocalDateTime.class); _localZoneId = Objects.requireNonNull(localZoneId); @@ -21,7 +35,13 @@ public LocalDateTimeValueReader(ZoneId localZoneId) { @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(), + // SimpleValueReader allows 'Date' objects to be null, so this should probably + // also be the case here. + if (p.hasToken(JsonToken.VALUE_NULL)) { + return null; + } + + final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(), ZonedDateTime::from, LocalDateTime::from); if (ta instanceof ZonedDateTime) { @@ -32,6 +52,9 @@ public Object read(JSONReader reader, JsonParser p) throws IOException { return ta; } - throw new IOException("Could not create a valid DateTime instance"); + throw new IOException(String.format("Converting \"%s\" to an instance of %s was " + + "unexpected and should not occur", + p.getText(), + ta.getClass().getSimpleName())); } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java index bf9c5eab..3da8a5ea 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java @@ -8,10 +8,15 @@ import com.fasterxml.jackson.jr.ob.api.ValueWriter; import com.fasterxml.jackson.jr.ob.impl.JSONWriter; +/** + * {@link ValueWriter} that converts a {@link LocalDateTime} to an ISO 8601 string without + * an offset or zone ID. + * @see ISO 8601 on Wikipedia + */ public class LocalDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { - String localDateTimeString = ((LocalDateTime) value).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + final String localDateTimeString = ((LocalDateTime) value).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); context.writeValue(localDateTimeString); } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java index 1cc6d455..4d7d180b 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java @@ -8,6 +8,12 @@ import com.fasterxml.jackson.jr.ob.api.ValueWriter; import com.fasterxml.jackson.jr.ob.impl.JSONWriter; +/** + * {@link ValueWriter} that converts a {@link OffsetDateTime} to an ISO 8601 string including + * an offset. + * @see ISO 8601 on Wikipedia + * @since 2.20 + */ public class OffsetDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java index cdddb388..39eee88b 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java @@ -8,6 +8,12 @@ import com.fasterxml.jackson.jr.ob.api.ValueWriter; import com.fasterxml.jackson.jr.ob.impl.JSONWriter; +/** + * {@link ValueWriter} that converts a {@link ZonedDateTime} to an ISO 8601 string including + * an offset and a zone ID. + * @see ISO 8601 on Wikipedia + * @since 2.20 + */ public class ZonedDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { diff --git a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java index 788f4c0d..6e653451 100644 --- a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java +++ b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java @@ -139,4 +139,18 @@ public void testWrite() throws JSONObjectException, IOException { Assertions.assertEquals("\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\"", zdtString); } + @Test + public void testNull() throws JSONObjectException, IOException { + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); + + final LocalDateTime local = json.beanFrom(LocalDateTime.class, "null"); + Assertions.assertNull(local); + + final OffsetDateTime offset = json.beanFrom(OffsetDateTime.class, "null"); + Assertions.assertNull(offset); + + final ZonedDateTime zoned = json.beanFrom(ZonedDateTime.class, "null"); + Assertions.assertNull(zoned); + } + } From f1fdc7cdfe8e37099ff92529ffda671e95b98b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stan=20Bron=C3=A9?= Date: Fri, 15 Aug 2025 11:49:34 +0200 Subject: [PATCH 4/6] Better exception handling when non strings are offered. ISO 8601 compliant behavior for ISO 8601 strings that lack an offset. --- .../javatime/DefaultDateTimeValueReader.java | 26 ++++++--- .../JavaTimeReaderWriterProvider.java | 43 +++++++++------ .../javatime/LocalDateTimeValueReader.java | 35 ++++++------ .../javatime/OffsetDateTimeValueReader.java | 43 +++++++++++++++ .../javatime/OffsetDateTimeValueWriter.java | 2 +- .../javatime/ZonedDateTimeValueReader.java | 42 ++++++++++++++ .../javatime/ZonedDateTimeValueWriter.java | 2 +- .../javatime/LocalDateTimeReaderTest.java | 55 ++++++++++++++++--- 8 files changed, 196 insertions(+), 52 deletions(-) create mode 100644 jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueReader.java create mode 100644 jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueReader.java diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java index eb7474df..eda0995a 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.jr.ob.JSONObjectException; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; @@ -23,25 +24,34 @@ public class DefaultDateTimeValueReader extends Valu private final TemporalQuery _query; /** - * Constructor that includes a temportal query that is to be used during formatting. + * Constructor that includes a temporal query that is to be used during formatting. * @param targetType Target type * @param query Temporal query for parsing */ public DefaultDateTimeValueReader(Class targetType, TemporalQuery query) { super(targetType); - - this._query = Objects.requireNonNull(query); + _query = Objects.requireNonNull(query); } @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - // SimpleValueReader allows 'Date' objects to be null, so this should probably - // also be the case here. - if (p.hasToken(JsonToken.VALUE_NULL)) { + if (p.hasToken(JsonToken.VALUE_NULL)) { return null; } - - return JavaTimeReaderWriterProvider.FORMATTER.parse(p.getText(), _query); + if (p.hasToken(JsonToken.VALUE_STRING)) { + return JavaTimeReaderWriterProvider.OFFSET_FORMATTER.parse(p.getText(), _query); + } + + throw JSONObjectException.from(p, + "Can not create a "+_valueType.getName()+" instance out of "+_tokenDesc(p)); } + /** + * Check if an ISO 8601 string contains a time offset or not. + * @param text ISO 8601 string + * @return Return true when an offset is missing, otherwise: false + */ + protected boolean offsetMissing(String text) { + return !text.matches(".*(Z|[+-]\\d{2}(:?\\d{2})?)$"); + } } \ No newline at end of file diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java index 8e40379e..c28a4aae 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/JavaTimeReaderWriterProvider.java @@ -20,25 +20,25 @@ */ public class JavaTimeReaderWriterProvider extends ReaderWriterProvider { - private ZoneId _fallbackLocalZoneId; + private ZoneId _localZoneId; - protected static final DateTimeFormatter FORMATTER = createFormatter(true); - protected static final DateTimeFormatter LOCAL_FORMATTER = createFormatter(false); + protected static final DateTimeFormatter OFFSET_FORMATTER = _createFormatter(true); + protected static final DateTimeFormatter LOCAL_FORMATTER = _createFormatter(false); public JavaTimeReaderWriterProvider() { - _fallbackLocalZoneId = ZoneId.systemDefault(); + withLocalTimeZone(null); } @Override public ValueReader findValueReader(JSONReader readContext, Class type) { if (LocalDateTime.class.isAssignableFrom(type)) { - return new LocalDateTimeValueReader(_fallbackLocalZoneId); + return new LocalDateTimeValueReader(_localZoneId); } if (OffsetDateTime.class.isAssignableFrom(type)) { - return new DefaultDateTimeValueReader(OffsetDateTime.class, OffsetDateTime::from); + return new OffsetDateTimeValueReader(_localZoneId); } if (ZonedDateTime.class.isAssignableFrom(type)) { - return new DefaultDateTimeValueReader(ZonedDateTime.class, ZonedDateTime::from); + return new ZonedDateTimeValueReader(_localZoneId); } return null; } @@ -59,29 +59,38 @@ public ValueWriter findValueWriter(JSONWriter writeContext, Class type) { /** * Setter to configure a time zone that is to be applied when a zoned ISO 8601 date time needs - * to be converted to a LocalDateTime. Can be set to null to apply - * the system default. - * @see java.time.LocalDateTime - * @param fallbackLocalZoneId Time zone to apply, or null + * to be converted to a {@link LocalDateTime}. Can be set to null to apply the + * UTC default. + * @param localZoneId Time zone to apply, or null * @since 2.20 * @return Reference for chaining */ - public JavaTimeReaderWriterProvider setLocalFallbackTimeZone(ZoneId fallbackLocalZoneId) { - _fallbackLocalZoneId = fallbackLocalZoneId == null ? ZoneId.systemDefault() : fallbackLocalZoneId; + public JavaTimeReaderWriterProvider withLocalTimeZone(ZoneId localZoneId) { + _localZoneId = localZoneId == null ? ZoneId.of("Z") : localZoneId; return this; } + /** + * Convenience method to quickly set the system default time zone as the preferred one. This + * is equivalent to calling: withLocalTimeZone(ZoneId.systemDefault()). + * @since 2.20 + * @return Reference for chaining + */ + public JavaTimeReaderWriterProvider withSystemDefaultTimeZone() { + return withLocalTimeZone(ZoneId.systemDefault()); + } + /** * Create a forgiving date time formatter that allows different interpretations of ISO 8601 * strings to be parsed. * * @param includeUtcDefault Set to true to set UTC to be the default offset - * for non-local date times. Set to false when handling - * local date times. + * for non-local date times that do not have an offset. Set to + * false when handling local date times. * @since 2.20 * @return Formatter */ - public static DateTimeFormatter createFormatter(boolean includeUtcDefault) { + private static DateTimeFormatter _createFormatter(boolean includeUtcDefault) { final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() .parseLenient() .appendPattern("yyyy-MM-dd'T'HH:mm:ss") @@ -98,6 +107,8 @@ public static DateTimeFormatter createFormatter(boolean includeUtcDefault) { .optionalEnd(); if (includeUtcDefault) { + // Without this, parsing a ZonedDateTime or OffsetDateTime will cause an exception if + // no offset was specified. So we default the offset to UTC to be safe. builder.parseDefaulting(ChronoField.OFFSET_SECONDS, 0); } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java index b1b3a381..6c249853 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueReader.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.jr.ob.JSONObjectException; import com.fasterxml.jackson.jr.ob.api.ValueReader; import com.fasterxml.jackson.jr.ob.impl.JSONReader; @@ -35,26 +36,24 @@ public LocalDateTimeValueReader(ZoneId localZoneId) { @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - // SimpleValueReader allows 'Date' objects to be null, so this should probably - // also be the case here. - if (p.hasToken(JsonToken.VALUE_NULL)) { + if (p.hasToken(JsonToken.VALUE_NULL)) { return null; } - - final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(), - ZonedDateTime::from, LocalDateTime::from); - - if (ta instanceof ZonedDateTime) { - // Convert a date time that unexpectedly includes a time offset or zone ID, to a proper local date time - return ((ZonedDateTime)ta).withZoneSameInstant(_localZoneId).toLocalDateTime(); + if (p.hasToken(JsonToken.VALUE_STRING)) { + final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(), + ZonedDateTime::from, LocalDateTime::from); + + if (ta instanceof ZonedDateTime) { + // Convert a date time that unexpectedly includes a time offset or zone ID, to a + // local date time + return ((ZonedDateTime)ta).withZoneSameInstant(_localZoneId).toLocalDateTime(); + } + if (ta instanceof LocalDateTime) { + return ta; + } } - if (ta instanceof LocalDateTime) { - return ta; - } - - throw new IOException(String.format("Converting \"%s\" to an instance of %s was " - + "unexpected and should not occur", - p.getText(), - ta.getClass().getSimpleName())); + + throw JSONObjectException.from(p, + "Can not create a "+_valueType.getName()+" instance out of "+_tokenDesc(p)); } } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueReader.java new file mode 100644 index 00000000..de088810 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueReader.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.jr.ob.api.ValueReader; +import com.fasterxml.jackson.jr.ob.impl.JSONReader; + +/** + * {@link ValueReader} that converts an ISO 8601 string to a {@link ZonedDateTime} instance. + * @see ISO 8601 on Wikipedia + * @since 2.20 + */ +public class OffsetDateTimeValueReader extends DefaultDateTimeValueReader { + private final ZoneId _localZoneId; + + /** + * Constructor that accepts a zone ID as a parameter. The zone ID is configured when no zone + * or offset was configured in the ISO 8601 string. + * @param localZoneId Local zone ID + */ + public OffsetDateTimeValueReader(ZoneId localZoneId) { + super(OffsetDateTime.class, OffsetDateTime::from); + _localZoneId = Objects.requireNonNull(localZoneId); + } + + @Override + public Object read(JSONReader reader, JsonParser p) throws IOException { + final OffsetDateTime odt = (OffsetDateTime)super.read(reader, p); + + // If the offset was missing, our formatter defaults to UTC. However, we need to set it + // to our preferred local zone to get ISO 8601 compliant behavior. In case our preferred + // zone already is UTC, no harm is done here. + if (odt != null && offsetMissing(p.getText())) { + return odt.withOffsetSameLocal(_localZoneId.getRules().getOffset(odt.toInstant())); + } + return odt; + } +} diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java index 4d7d180b..00216e74 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java @@ -17,7 +17,7 @@ public class OffsetDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { - String offsetDateTimeString = ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + final String offsetDateTimeString = ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); context.writeValue(offsetDateTimeString); } diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueReader.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueReader.java new file mode 100644 index 00000000..47f61320 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueReader.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.jr.extension.javatime; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.jr.ob.api.ValueReader; +import com.fasterxml.jackson.jr.ob.impl.JSONReader; + +/** + * {@link ValueReader} that converts an ISO 8601 string to a {@link ZonedDateTime} instance. + * @see ISO 8601 on Wikipedia + * @since 2.20 + */ +public class ZonedDateTimeValueReader extends DefaultDateTimeValueReader { + private final ZoneId _localZoneId; + + /** + * Constructor that accepts a zone ID as a parameter. The zone ID is configured when no zone + * or offset was configured in the ISO 8601 string. + * @param localZoneId Local zone ID + */ + public ZonedDateTimeValueReader(ZoneId localZoneId) { + super(ZonedDateTime.class, ZonedDateTime::from); + _localZoneId = Objects.requireNonNull(localZoneId); + } + + @Override + public Object read(JSONReader reader, JsonParser p) throws IOException { + final ZonedDateTime zdt = (ZonedDateTime)super.read(reader, p); + + // If the offset was missing, our formatter defaults to UTC. However, we need to set it + // to our preferred local zone to get ISO 8601 compliant behavior. In case our preferred + // zone already is UTC, no harm is done here. + if (zdt != null && offsetMissing(p.getText())) { + return zdt.withZoneSameLocal(_localZoneId); + } + return zdt; + } +} diff --git a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java index 39eee88b..096febf9 100644 --- a/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java @@ -17,7 +17,7 @@ public class ZonedDateTimeValueWriter implements ValueWriter { @Override public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException { - String zonedDateTimeString = ((ZonedDateTime) value).format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + final String zonedDateTimeString = ((ZonedDateTime) value).format(DateTimeFormatter.ISO_ZONED_DATE_TIME); context.writeValue(zonedDateTimeString); } diff --git a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java index 6e653451..bc57a69c 100644 --- a/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java +++ b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java @@ -19,7 +19,7 @@ public class LocalDateTimeReaderTest { @Test public void testRead() throws JSONObjectException, IOException { final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension() - .with(new JavaTimeReaderWriterProvider().setLocalFallbackTimeZone(ZoneId.of("Europe/Berlin")))) + .with(new JavaTimeReaderWriterProvider().withLocalTimeZone(ZoneId.of("Europe/Berlin")))) .build(); final LocalDateTime local = json.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15.123456789\""); @@ -73,16 +73,16 @@ public void testRead() throws JSONObjectException, IOException { Assertions.assertEquals(123, offsetted.getLong(ChronoField.MILLI_OF_SECOND)); Assertions.assertEquals(7200, offsetted.getOffset().getTotalSeconds()); - final OffsetDateTime offsettedtWithoutOffset = json.beanFrom(OffsetDateTime.class, "\"2025-08-04T12:34:15.123\""); + final OffsetDateTime offsettedtWithoutOffset = json.beanFrom(OffsetDateTime.class, "\"2025-08-04T14:34:15.123\""); Assertions.assertEquals(2025, offsettedtWithoutOffset.getYear()); Assertions.assertEquals(8, offsettedtWithoutOffset.getMonthValue()); Assertions.assertEquals(4, offsettedtWithoutOffset.getDayOfMonth()); - Assertions.assertEquals(12, offsettedtWithoutOffset.getHour()); + Assertions.assertEquals(14, offsettedtWithoutOffset.getHour()); Assertions.assertEquals(34, offsettedtWithoutOffset.getMinute()); Assertions.assertEquals(15, offsettedtWithoutOffset.getSecond()); Assertions.assertEquals(123, offsettedtWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); - Assertions.assertEquals(0, offsettedtWithoutOffset.getOffset().getTotalSeconds()); + Assertions.assertEquals(7200, offsettedtWithoutOffset.getOffset().getTotalSeconds()); final ZonedDateTime zoned = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123+02:00[Europe/Berlin]\""); @@ -107,12 +107,51 @@ public void testRead() throws JSONObjectException, IOException { Assertions.assertEquals(123, zonedWithoutZoneName.getLong(ChronoField.MILLI_OF_SECOND)); Assertions.assertEquals(7200, zonedWithoutZoneName.getOffset().getTotalSeconds()); - final ZonedDateTime zonedWithoutOffset = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T12:34:15.123\""); + final ZonedDateTime zonedWithoutOffset = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T14:34:15.123\""); + + Assertions.assertEquals(2025, zonedWithoutOffset.getYear()); + Assertions.assertEquals(8, zonedWithoutOffset.getMonthValue()); + Assertions.assertEquals(4, zonedWithoutOffset.getDayOfMonth()); + Assertions.assertEquals(14, zonedWithoutOffset.getHour()); + Assertions.assertEquals(34, zonedWithoutOffset.getMinute()); + Assertions.assertEquals(15, zonedWithoutOffset.getSecond()); + Assertions.assertEquals(123, zonedWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(7200, zonedWithoutOffset.getOffset().getTotalSeconds()); + } + + @Test + public void testReadUtc() throws JSONObjectException, IOException { + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension() + .with(new JavaTimeReaderWriterProvider())) + .build(); + + final LocalDateTime local = json.beanFrom(LocalDateTime.class, "\"2025-08-04T14:34:15.123456789\""); + + Assertions.assertEquals(2025, local.getYear()); + Assertions.assertEquals(8, local.getMonthValue()); + Assertions.assertEquals(4, local.getDayOfMonth()); + Assertions.assertEquals(14, local.getHour()); + Assertions.assertEquals(34, local.getMinute()); + Assertions.assertEquals(15, local.getSecond()); + Assertions.assertEquals(123, local.getLong(ChronoField.MILLI_OF_SECOND)); + + final OffsetDateTime offsettedtWithoutOffset = json.beanFrom(OffsetDateTime.class, "\"2025-08-04T14:34:15.123\""); + + Assertions.assertEquals(2025, offsettedtWithoutOffset.getYear()); + Assertions.assertEquals(8, offsettedtWithoutOffset.getMonthValue()); + Assertions.assertEquals(4, offsettedtWithoutOffset.getDayOfMonth()); + Assertions.assertEquals(14, offsettedtWithoutOffset.getHour()); + Assertions.assertEquals(34, offsettedtWithoutOffset.getMinute()); + Assertions.assertEquals(15, offsettedtWithoutOffset.getSecond()); + Assertions.assertEquals(123, offsettedtWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); + Assertions.assertEquals(0, offsettedtWithoutOffset.getOffset().getTotalSeconds()); + + final ZonedDateTime zonedWithoutOffset = json.beanFrom(ZonedDateTime.class, "\"2025-08-04T14:34:15.123\""); Assertions.assertEquals(2025, zonedWithoutOffset.getYear()); Assertions.assertEquals(8, zonedWithoutOffset.getMonthValue()); Assertions.assertEquals(4, zonedWithoutOffset.getDayOfMonth()); - Assertions.assertEquals(12, zonedWithoutOffset.getHour()); + Assertions.assertEquals(14, zonedWithoutOffset.getHour()); Assertions.assertEquals(34, zonedWithoutOffset.getMinute()); Assertions.assertEquals(15, zonedWithoutOffset.getSecond()); Assertions.assertEquals(123, zonedWithoutOffset.getLong(ChronoField.MILLI_OF_SECOND)); @@ -141,9 +180,9 @@ public void testWrite() throws JSONObjectException, IOException { @Test public void testNull() throws JSONObjectException, IOException { - final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension()).build(); - final LocalDateTime local = json.beanFrom(LocalDateTime.class, "null"); + final LocalDateTime local = json.beanFrom(LocalDateTime.class, "null"); Assertions.assertNull(local); final OffsetDateTime offset = json.beanFrom(OffsetDateTime.class, "null"); From 4d257e349ca7adc4e28d30110f019a92edb14ca8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 17 Aug 2025 10:23:43 -0700 Subject: [PATCH 5/6] Add release notes --- release-notes/CREDITS-2.x | 10 +++++++++- release-notes/VERSION-2.x | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 97e59c76..280c16f1 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -78,5 +78,13 @@ Giovanni van der Schelde (@Giovds) Luke Hutchison (@lukehutch) -* Reported #196: `float[]` and `double[]` are serialized to JSON as `{ }` +* Reported #196: `float[]` and `double[]` are serialized to JSON as `{ (2.20.0) + +Stan Brone (@StanB-EKZ) + +* Contributed #201: Support for OffsetDateTime and ZonedDateTime + in jackson-jr-extension-javatime + (2.20.0) + + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 29b367a5..44bbb897 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -13,6 +13,8 @@ Modules: #196: `float[]` and `double[]` are serialized to JSON as `{ }` (contributed by Luke H) +#201: Support for OffsetDateTime and ZonedDateTime in jackson-jr-extension-javatime + (contributed by Stan B) 2.20.0-rc1 (04-Aug-2025) From f8709b008b11750562742da1c5315da44107575c Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 17 Aug 2025 10:26:28 -0700 Subject: [PATCH 6/6] Update READMEs too --- README.md | 2 +- jr-extension-javatime/README.md | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c477aeb..f997c0cf 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Project is composed of multiple Maven sub-modules, each corresponding to a jar: * [jr-retrofit2](../../tree/master/jr-retrofit2) contains `jackson-jr` - based handlers for [Retrofit 2](https://square.github.io/retrofit/) library * Depends on `jackson-jr` and `Retrofit` API jars, and indirectly on `jackson-core` * [jr-annotation-support](../../tree/master/jr-annotation-support) contains extension with support for a subset of core [Jackson annotations](../../../jackson-annotations) -* [jr-extension-javatime](../../tree/master/jr-extension-javatime) contains extension with support for a subset of Java 8 Date/Time types (e.g. `java.time.LocalDateTime`) +* [jr-extension-javatime](../../tree/master/jr-extension-javatime) contains extension with support for a subset of Java 8 Date/Time types (e.g. `java.time.LocalDateTime`, `java.time.OffsetDateTime` (2.20), `java.time.ZonedDateTime` (2.20)) * jr-all creates an "uber-jar" that contains individual modules along with all their dependencies: * `jr-objects` classes as-is, without relocating * `jr-stree` classes as-is, without relocating diff --git a/jr-extension-javatime/README.md b/jr-extension-javatime/README.md index 12ef32e5..34654c50 100644 --- a/jr-extension-javatime/README.md +++ b/jr-extension-javatime/README.md @@ -4,7 +4,7 @@ This module extends the functionality of jackson-jr by adding support for (a sub ### Status -Added in Jackson 2.17. +Added in Jackson 2.17; extended in 2.20. ### Usage To be able to use supported annotations, you need to register extension like so: @@ -48,7 +48,10 @@ public class MyClass { ### Date Classes currently supported by `JacksonJrJavaTimeExtension` - `java.time.LocalDateTime` +- `java.time.OffsetDateTime` (2.20+) +- `java.time.ZonedDateTime` (2.20+) ### Plans for Future -- Add support for other Java 8 Date/Time types +- Add support for more Java 8 Date/Time types +