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 + 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..eda0995a --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/DefaultDateTimeValueReader.java @@ -0,0 +1,57 @@ +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.JSONObjectException; +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 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); + _query = Objects.requireNonNull(query); + } + + @Override + public Object read(JSONReader reader, JsonParser p) throws IOException { + if (p.hasToken(JsonToken.VALUE_NULL)) { + return null; + } + 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/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..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 @@ -1,48 +1,117 @@ package com.fasterxml.jackson.jr.extension.javatime; +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; +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; - - public JavaTimeReaderWriterProvider() { } + private ZoneId _localZoneId; + + protected static final DateTimeFormatter OFFSET_FORMATTER = _createFormatter(true); + protected static final DateTimeFormatter LOCAL_FORMATTER = _createFormatter(false); + + public JavaTimeReaderWriterProvider() { + withLocalTimeZone(null); + } @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(_localZoneId); + } + if (OffsetDateTime.class.isAssignableFrom(type)) { + return new OffsetDateTimeValueReader(_localZoneId); + } + if (ZonedDateTime.class.isAssignableFrom(type)) { + return new ZonedDateTimeValueReader(_localZoneId); + } + return null; } @Override public ValueWriter findValueWriter(JSONWriter writeContext, Class type) { - return LocalDateTime.class.isAssignableFrom(type) ? new LocalDateTimeValueWriter(dateTimeFormatter) : null; + 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; } - + /** - * 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 + * Setter to configure a time zone that is to be applied when a zoned ISO 8601 date time needs + * 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 withDateTimeFormatter(DateTimeFormatter formatter) { - dateTimeFormatter = formatter; + 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 that do not have an offset. Set to + * false when handling local date times. + * @since 2.20 + * @return Formatter + */ + private 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) { + // 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); + } + + 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 cc133327..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 @@ -1,23 +1,59 @@ 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 java.util.Objects; + 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; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - +/** + * {@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 DateTimeFormatter formatter; - - public LocalDateTimeValueReader(DateTimeFormatter formatter) { + 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); - this.formatter = formatter; + _localZoneId = Objects.requireNonNull(localZoneId); } @Override public Object read(JSONReader reader, JsonParser p) throws IOException { - return LocalDateTime.parse(p.getText(), formatter); + if (p.hasToken(JsonToken.VALUE_NULL)) { + return null; + } + 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; + } + } + + 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/LocalDateTimeValueWriter.java b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeValueWriter.java index 020963f6..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 @@ -1,23 +1,22 @@ 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; +/** + * {@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(formatter); + 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/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 new file mode 100644 index 00000000..00216e74 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/OffsetDateTimeValueWriter.java @@ -0,0 +1,28 @@ +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; + +/** + * {@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 { + final 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/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 new file mode 100644 index 00000000..096febf9 --- /dev/null +++ b/jr-extension-javatime/src/main/java/com/fasterxml/jackson/jr/extension/javatime/ZonedDateTimeValueWriter.java @@ -0,0 +1,28 @@ +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; + +/** + * {@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 { + final 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..bc57a69c --- /dev/null +++ b/jr-extension-javatime/src/test/java/com/fasterxml/jackson/jr/extension/javatime/LocalDateTimeReaderTest.java @@ -0,0 +1,195 @@ +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 { + + @Test + public void testRead() throws JSONObjectException, IOException { + final JSON json = JSON.builder().register(new JacksonJrJavaTimeExtension() + .with(new JavaTimeReaderWriterProvider().withLocalTimeZone(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-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(7200, 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-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(14, 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); + } + + @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); + } + +} 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)