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)