diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index ebc26ed986..3e63715106 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -35,6 +35,11 @@
exports tools.jackson.databind.deser.std;
exports tools.jackson.databind.exc;
exports tools.jackson.databind.ext;
+ exports tools.jackson.databind.ext.javatime;
+ exports tools.jackson.databind.ext.javatime.deser;
+ exports tools.jackson.databind.ext.javatime.deser.key;
+ exports tools.jackson.databind.ext.javatime.ser;
+ exports tools.jackson.databind.ext.javatime.ser.key;
exports tools.jackson.databind.ext.jdk8;
// Needed by Ion module for SqlDate deserializer:
exports tools.jackson.databind.ext.sql;
diff --git a/src/main/java/tools/jackson/databind/JacksonModule.java b/src/main/java/tools/jackson/databind/JacksonModule.java
index b279aa2900..1ef63632d7 100644
--- a/src/main/java/tools/jackson/databind/JacksonModule.java
+++ b/src/main/java/tools/jackson/databind/JacksonModule.java
@@ -5,8 +5,7 @@
import java.util.function.UnaryOperator;
import tools.jackson.core.*;
-import tools.jackson.databind.cfg.MapperBuilder;
-import tools.jackson.databind.cfg.MutableConfigOverride;
+import tools.jackson.databind.cfg.*;
import tools.jackson.databind.deser.*;
import tools.jackson.databind.jsontype.NamedType;
import tools.jackson.databind.ser.Serializers;
@@ -146,6 +145,9 @@ public static interface SetupContext
public boolean isEnabled(TokenStreamFactory.Feature f);
public boolean isEnabled(StreamReadFeature f);
public boolean isEnabled(StreamWriteFeature f);
+ public boolean isEnabled(DatatypeFeature f);
+
+ public DatatypeFeatures datatypeFeatures();
/*
/******************************************************************
diff --git a/src/main/java/tools/jackson/databind/SerializationFeature.java b/src/main/java/tools/jackson/databind/SerializationFeature.java
index 15f1e86891..e070b5e45f 100644
--- a/src/main/java/tools/jackson/databind/SerializationFeature.java
+++ b/src/main/java/tools/jackson/databind/SerializationFeature.java
@@ -210,6 +210,21 @@ public enum SerializationFeature implements ConfigFeature
*/
WRITE_DATE_KEYS_AS_TIMESTAMPS(false),
+ /**
+ * Feature that controls whether numeric timestamp values are
+ * to be written using nanosecond timestamps (enabled) or not (disabled);
+ * if and only if datatype supports such resolution.
+ * Only newer datatypes (such as Java8 Date/Time) support such resolution --
+ * older types (pre-Java8 java.util.Date etc) and Joda do not --
+ * and this setting has no effect on such types.
+ *
+ * If disabled, standard millisecond timestamps are assumed.
+ * This is the counterpart to {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}.
+ *
+ * Feature is enabled by default, to support most accurate time values possible.
+ */
+ WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS(true),
+
/**
* Feature that determines whether date/date-time values should be serialized
* so that they include timezone id, in cases where type itself contains
@@ -360,21 +375,6 @@ public enum SerializationFeature implements ConfigFeature
*/
WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED(false),
- /**
- * Feature that controls whether numeric timestamp values are
- * to be written using nanosecond timestamps (enabled) or not (disabled);
- * if and only if datatype supports such resolution.
- * Only newer datatypes (such as Java8 Date/Time) support such resolution --
- * older types (pre-Java8 java.util.Date etc) and Joda do not --
- * and this setting has no effect on such types.
- *
- * If disabled, standard millisecond timestamps are assumed.
- * This is the counterpart to {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}.
- *
- * Feature is enabled by default, to support most accurate time values possible.
- */
- WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS(true),
-
/**
* Feature that determines whether {@link java.util.Map} entries are first
* sorted by key before serialization or not: if enabled, additional sorting
diff --git a/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java b/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java
index c8621160ea..f3c506c074 100644
--- a/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java
+++ b/src/main/java/tools/jackson/databind/cfg/DatatypeFeatures.java
@@ -14,17 +14,21 @@ public class DatatypeFeatures
protected final static int FEATURE_INDEX_ENUM = 0;
protected final static int FEATURE_INDEX_JSON_NODE = 1;
+ protected final static int FEATURE_INDEX_DATETIME = 2;
- private final int _enabledFor1, _enabledFor2;
+ private final int _enabledFor1, _enabledFor2, _enabledFor3;
- private final int _explicitFor1, _explicitFor2;
+ private final int _explicitFor1, _explicitFor2, _explicitFor3;
protected DatatypeFeatures(int enabledFor1, int explicitFor1,
- int enabledFor2, int explicitFor2) {
+ int enabledFor2, int explicitFor2,
+ int enabledFor3, int explicitFor3) {
_enabledFor1 = enabledFor1;
_explicitFor1 = explicitFor1;
_enabledFor2 = enabledFor2;
_explicitFor2 = explicitFor2;
+ _enabledFor3 = enabledFor3;
+ _explicitFor3 = explicitFor3;
}
public static DatatypeFeatures defaultFeatures() {
@@ -32,13 +36,16 @@ public static DatatypeFeatures defaultFeatures() {
}
private DatatypeFeatures _with(int enabledFor1, int explicitFor1,
- int enabledFor2, int explicitFor2) {
+ int enabledFor2, int explicitFor2,
+ int enabledFor3, int explicitFor3) {
if ((_enabledFor1 == enabledFor1) && (_explicitFor1 == explicitFor1)
- && (_enabledFor2 == enabledFor2) && (_explicitFor2 == explicitFor2)) {
+ && (_enabledFor2 == enabledFor2) && (_explicitFor2 == explicitFor2)
+ && (_enabledFor3 == enabledFor3) && (_explicitFor3 == explicitFor3)) {
return this;
}
return new DatatypeFeatures(enabledFor1, explicitFor1,
- enabledFor2, explicitFor2);
+ enabledFor2, explicitFor2,
+ enabledFor3, explicitFor3);
}
/*
@@ -61,10 +68,16 @@ public DatatypeFeatures with(DatatypeFeature f) {
switch (f.featureIndex()) {
case 0:
return _with(_enabledFor1 | mask, _explicitFor1 | mask,
- _enabledFor2, _explicitFor2);
+ _enabledFor2, _explicitFor2,
+ _enabledFor3, _explicitFor3);
case 1:
return _with(_enabledFor1, _explicitFor1,
- _enabledFor2 | mask, _explicitFor2 | mask);
+ _enabledFor2 | mask, _explicitFor2 | mask,
+ _enabledFor3, _explicitFor3);
+ case 2:
+ return _with(_enabledFor1, _explicitFor1,
+ _enabledFor2, _explicitFor2,
+ _enabledFor3 | mask, _explicitFor3 | mask);
default:
VersionUtil.throwInternal();
return this;
@@ -88,10 +101,16 @@ public DatatypeFeatures withFeatures(DatatypeFeature... features) {
switch (features[0].featureIndex()) {
case 0:
return _with(_enabledFor1 | mask, _explicitFor1 | mask,
- _enabledFor2, _explicitFor2);
+ _enabledFor2, _explicitFor2,
+ _enabledFor3, _explicitFor3);
case 1:
return _with(_enabledFor1, _explicitFor1,
- _enabledFor2 | mask, _explicitFor2 | mask);
+ _enabledFor2 | mask, _explicitFor2 | mask,
+ _enabledFor3, _explicitFor3);
+ case 2:
+ return _with(_enabledFor1, _explicitFor1,
+ _enabledFor2, _explicitFor2,
+ _enabledFor3 | mask, _explicitFor3 | mask);
default:
VersionUtil.throwInternal();
return this;
@@ -112,10 +131,16 @@ public DatatypeFeatures without(DatatypeFeature f) {
switch (f.featureIndex()) {
case 0:
return _with(_enabledFor1 & ~mask, _explicitFor1 | mask,
- _enabledFor2, _explicitFor2);
+ _enabledFor2, _explicitFor2,
+ _enabledFor3, _explicitFor3);
case 1:
return _with(_enabledFor1, _explicitFor1,
- _enabledFor2 & ~mask, _explicitFor2 | mask);
+ _enabledFor2 & ~mask, _explicitFor2 | mask,
+ _enabledFor3, _explicitFor3);
+ case 2:
+ return _with(_enabledFor1, _explicitFor1,
+ _enabledFor2, _explicitFor2,
+ _enabledFor3 & ~mask, _explicitFor3 | mask);
default:
VersionUtil.throwInternal();
return this;
@@ -139,10 +164,16 @@ public DatatypeFeatures withoutFeatures(DatatypeFeature... features) {
switch (features[0].featureIndex()) {
case 0:
return _with(_enabledFor1 & ~mask, _explicitFor1 | mask,
- _enabledFor2, _explicitFor2);
+ _enabledFor2, _explicitFor2,
+ _enabledFor3, _explicitFor3);
case 1:
return _with(_enabledFor1, _explicitFor1,
- _enabledFor2 & ~mask, _explicitFor2 | mask);
+ _enabledFor2 & ~mask, _explicitFor2 | mask,
+ _enabledFor3, _explicitFor3);
+ case 2:
+ return _with(_enabledFor1, _explicitFor1,
+ _enabledFor2, _explicitFor2,
+ _enabledFor3 & ~mask, _explicitFor3 | mask);
default:
VersionUtil.throwInternal();
return this;
@@ -179,6 +210,8 @@ public boolean isEnabled(DatatypeFeature f) {
return f.enabledIn(_enabledFor1);
case 1:
return f.enabledIn(_enabledFor2);
+ case 2:
+ return f.enabledIn(_enabledFor3);
default:
VersionUtil.throwInternal();
return false;
@@ -200,6 +233,8 @@ public boolean isExplicitlySet(DatatypeFeature f) {
return f.enabledIn(_explicitFor1);
case 1:
return f.enabledIn(_explicitFor2);
+ case 2:
+ return f.enabledIn(_explicitFor3);
default:
VersionUtil.throwInternal();
return false;
@@ -215,8 +250,6 @@ public boolean isExplicitlySet(DatatypeFeature f) {
* @param f Feature to check
*
* @return Whether given feature has been explicitly enabled
- *
- * @since 2.15
*/
public boolean isExplicitlyEnabled(DatatypeFeature f) {
switch (f.featureIndex()) {
@@ -224,6 +257,8 @@ public boolean isExplicitlyEnabled(DatatypeFeature f) {
return f.enabledIn(_explicitFor1 & _enabledFor1);
case 1:
return f.enabledIn(_explicitFor2 & _enabledFor2);
+ case 2:
+ return f.enabledIn(_explicitFor3 & _enabledFor3);
default:
VersionUtil.throwInternal();
return false;
@@ -239,8 +274,6 @@ public boolean isExplicitlyEnabled(DatatypeFeature f) {
* @param f Feature to check
*
* @return Whether given feature has been explicitly disabled
- *
- * @since 2.15
*/
public boolean isExplicitlyDisabled(DatatypeFeature f) {
switch (f.featureIndex()) {
@@ -248,6 +281,8 @@ public boolean isExplicitlyDisabled(DatatypeFeature f) {
return f.enabledIn(_explicitFor1 & ~_enabledFor1);
case 1:
return f.enabledIn(_explicitFor2 & ~_enabledFor2);
+ case 2:
+ return f.enabledIn(_explicitFor3 & ~_enabledFor3);
default:
VersionUtil.throwInternal();
return false;
@@ -276,6 +311,11 @@ public Boolean getExplicitState(DatatypeFeature f) {
return f.enabledIn(_enabledFor2);
}
return null;
+ case 2:
+ if (f.enabledIn(_explicitFor3)) {
+ return f.enabledIn(_enabledFor3);
+ }
+ return null;
default:
VersionUtil.throwInternal();
return null;
@@ -298,7 +338,8 @@ private static class DefaultHolder
static {
DEFAULT_FEATURES = new DatatypeFeatures(
collectDefaults(EnumFeature.values()), 0,
- collectDefaults(JsonNodeFeature.values()), 0
+ collectDefaults(JsonNodeFeature.values()), 0,
+ collectDefaults(DateTimeFeature.values()), 0
);
}
diff --git a/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java
new file mode 100644
index 0000000000..e8193db7e2
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java
@@ -0,0 +1,84 @@
+package tools.jackson.databind.cfg;
+
+/**
+ * Configurable on/off features to configure Date/Time handling.
+ * Mostly used to configure
+ * Java 8 Time ({@code java.time}) type handling (see
+ * {@link tools.jackson.databind.ext.javatime.JavaTimeInitializer})
+ * but also to "legacy" ({@link java.util.Date}, {@link java.util.Calendar})
+ * and Joda Date/Time.
+ */
+public enum DateTimeFeature implements DatatypeFeature
+{
+ /**
+ * Feature that determines whether {@link java.time.ZoneId} is normalized
+ * (via call to {@code java.time.ZoneId#normalized()}) when deserializing
+ * types like {@link java.time.ZonedDateTime}.
+ *
+ * Default setting is enabled, for backwards-compatibility with
+ * Jackson 2.15.
+ */
+ NORMALIZE_DESERIALIZED_ZONE_ID(true),
+
+ /**
+ * Feature that determines whether the {@link java.util.TimeZone} of the
+ * {@link tools.jackson.databind.DeserializationContext} is used
+ * when leniently deserializing {@link java.time.LocalDate} or
+ * {@link java.time.LocalDateTime} from the UTC/ISO instant format.
+ *
+ * Default setting is disabled, for backwards-compatibility with
+ * Jackson 2.18.
+ *
+ * @since 2.19
+ */
+ USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING(false),
+
+ /**
+ * Feature that controls whether stringified numbers (Strings that without
+ * quotes would be legal JSON Numbers) may be interpreted as
+ * timestamps (enabled) or not (disabled), in case where there is an
+ * explicitly defined pattern ({@code DateTimeFormatter}, usually by
+ * using {@code @JsonFormat} annotation) for value.
+ *
+ * Note that when the default pattern is used (no custom pattern defined),
+ * stringified numbers are always accepted as timestamps regardless of
+ * this feature.
+ */
+ ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false),
+
+ /**
+ * Feature that determines whether {@link java.time.Month} is serialized
+ * and deserialized as using a zero-based index (FALSE) or a one-based index (TRUE).
+ * For example, "1" would be serialized/deserialized as Month.JANUARY if TRUE and Month.FEBRUARY if FALSE.
+ *
+ * Default setting is false, meaning that Month is serialized/deserialized as a zero-based index.
+ */
+ ONE_BASED_MONTHS(false)
+ ;
+
+ private final static int FEATURE_INDEX = DatatypeFeatures.FEATURE_INDEX_DATETIME;
+
+ /**
+ * Whether feature is enabled or disabled by default.
+ */
+ private final boolean _enabledByDefault;
+
+ private final int _mask;
+
+ private DateTimeFeature(boolean enabledByDefault) {
+ _enabledByDefault = enabledByDefault;
+ _mask = (1 << ordinal());
+ }
+
+ @Override
+ public boolean enabledByDefault() { return _enabledByDefault; }
+ @Override
+ public boolean enabledIn(int flags) { return (flags & _mask) != 0; }
+ @Override
+ public int getMask() { return _mask; }
+
+ @Override
+ public int featureIndex() {
+ return FEATURE_INDEX;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java b/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java
index b61652d35f..063d0b2c48 100644
--- a/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java
+++ b/src/main/java/tools/jackson/databind/cfg/MapperBuilder.java
@@ -16,6 +16,7 @@
import tools.jackson.core.util.Snapshottable;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.*;
+import tools.jackson.databind.ext.javatime.JavaTimeInitializer;
import tools.jackson.databind.introspect.*;
import tools.jackson.databind.jsontype.*;
import tools.jackson.databind.jsontype.impl.DefaultTypeResolverBuilder;
@@ -419,8 +420,9 @@ public MapperBuilderState saveStateApplyModules()
{
if (_savedState == null) {
_savedState = _saveState();
+ ModuleContextBase ctxt = _constructModuleContext();
+ JavaTimeInitializer.getInstance().setupModule(ctxt);
if (_modules != null) {
- ModuleContextBase ctxt = _constructModuleContext();
_modules.values().forEach(m -> m.setupModule(ctxt));
// and since context may buffer some changes, ensure those are flushed:
ctxt.applyChanges(this);
@@ -485,7 +487,6 @@ public boolean isEnabled(SerializationFeature f) {
public boolean isEnabled(DatatypeFeature f) {
return _datatypeFeatures.isEnabled(f);
}
-
public boolean isEnabled(StreamReadFeature f) {
return f.enabledIn(_streamReadFeatures);
}
diff --git a/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java b/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java
index 7476564126..efce0cbef1 100644
--- a/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java
+++ b/src/main/java/tools/jackson/databind/cfg/ModuleContextBase.java
@@ -4,6 +4,7 @@
import java.util.function.UnaryOperator;
import tools.jackson.core.*;
+
import tools.jackson.databind.*;
import tools.jackson.databind.JacksonModule.SetupContext;
import tools.jackson.databind.deser.*;
@@ -125,6 +126,16 @@ public boolean isEnabled(StreamWriteFeature f) {
return _builder.isEnabled(f);
}
+ @Override
+ public boolean isEnabled(DatatypeFeature f) {
+ return _builder.isEnabled(f);
+ }
+
+ @Override
+ public DatatypeFeatures datatypeFeatures() {
+ return _builder.datatypeFeatures();
+ }
+
/*
/**********************************************************************
/* Mutators for adding deserializers, related
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java
new file mode 100644
index 0000000000..5c84100aba
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/JavaTimeInitializer.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime;
+
+import java.time.*;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.cfg.DatatypeFeatures;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.deser.ValueInstantiator;
+import tools.jackson.databind.deser.ValueInstantiators;
+import tools.jackson.databind.deser.std.StdValueInstantiator;
+import tools.jackson.databind.ext.javatime.deser.*;
+import tools.jackson.databind.ext.javatime.deser.key.*;
+import tools.jackson.databind.ext.javatime.ser.*;
+import tools.jackson.databind.ext.javatime.ser.key.ZonedDateTimeKeySerializer;
+import tools.jackson.databind.introspect.AnnotatedClass;
+import tools.jackson.databind.introspect.AnnotatedClassResolver;
+import tools.jackson.databind.introspect.AnnotatedMethod;
+import tools.jackson.databind.module.SimpleDeserializers;
+import tools.jackson.databind.module.SimpleKeyDeserializers;
+import tools.jackson.databind.module.SimpleSerializers;
+import tools.jackson.databind.ser.std.ToStringSerializer;
+
+/**
+ * Class that registers capability of serializing {@code java.time} objects with the Jackson core.
+ *
+ * In Jackson 3, the module is embedded in databind and handlers are automatically
+ * registered: approach is similar to one used by full {@link JacksonModule}s.
+ *
+ * Most {@code java.time} types are serialized as numbers (integers or decimals as appropriate) if the
+ * {@link tools.jackson.databind.SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} feature is enabled
+ * (or, for {@link Duration}, {@link tools.jackson.databind.SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS}),
+ * and otherwise are serialized in standard
+ * ISO-8601 string representation.
+ * ISO-8601 specifies formats for representing offset dates and times, zoned dates and times,
+ * local dates and times, periods, durations, zones, and more. All {@code java.time} types
+ * have built-in translation to and from ISO-8601 formats.
+ *
+ * Granularity of timestamps is controlled through the companion features
+ * {@link tools.jackson.databind.SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} and
+ * {@link tools.jackson.databind.DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS}. For serialization, timestamps are
+ * written as fractional numbers (decimals), where the number is seconds and the decimal is fractional seconds, if
+ * {@code WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is enabled (it is by default), with resolution as fine as nanoseconds depending on the
+ * underlying JDK implementation. If {@code WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is disabled, timestamps are written as a whole number of
+ * milliseconds. At deserialization time, decimal numbers are always read as fractional second timestamps with up-to-nanosecond resolution,
+ * since the meaning of the decimal is unambiguous. The more ambiguous integer types are read as fractional seconds without a decimal point
+ * if {@code READ_DATE_TIMESTAMPS_AS_NANOSECONDS} is enabled (it is by default), and otherwise they are read as milliseconds.
+ *
+{
+ public static final DurationDeserializer INSTANCE = new DurationDeserializer();
+
+ /**
+ * When defined (not {@code null}) integer values will be converted into duration
+ * unit configured for the converter.
+ * Using this converter will typically override the value specified in
+ * {@link DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS} as it is
+ * considered that the unit set in {@link JsonFormat#pattern()} has precedence
+ * since it is more specific.
+ *
+ * See [jackson-modules-java8#184] for more info.
+ */
+ protected final DurationUnitConverter _durationUnitConverter;
+
+ /**
+ * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ *
+ * @since 2.16
+ */
+ protected final Boolean _readTimestampsAsNanosOverride;
+
+ public DurationDeserializer() {
+ super(Duration.class);
+ _durationUnitConverter = null;
+ _readTimestampsAsNanosOverride = null;
+ }
+
+ /**
+ * @since 2.11
+ */
+ protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _durationUnitConverter = base._durationUnitConverter;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ }
+
+ /**
+ * @since 2.12
+ */
+ protected DurationDeserializer(DurationDeserializer base, DurationUnitConverter converter) {
+ super(base, base._isLenient);
+ _durationUnitConverter = converter;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ }
+
+ /**
+ * @since 2.16
+ */
+ protected DurationDeserializer(DurationDeserializer base,
+ Boolean leniency,
+ DurationUnitConverter converter,
+ Boolean readTimestampsAsNanosOverride) {
+ super(base, leniency);
+ _durationUnitConverter = converter;
+ _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ }
+
+ @Override
+ protected DurationDeserializer withLeniency(Boolean leniency) {
+ return new DurationDeserializer(this, leniency);
+ }
+
+ protected DurationDeserializer withConverter(DurationUnitConverter converter) {
+ return new DurationDeserializer(this, converter);
+ }
+
+ @Override
+ public ValueDeserializer> createContextual(DeserializationContext ctxt,
+ BeanProperty property)
+ {
+ JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
+ DurationDeserializer deser = this;
+ boolean leniency = _isLenient;
+ DurationUnitConverter unitConverter = _durationUnitConverter;
+ Boolean timestampsAsNanosOverride = _readTimestampsAsNanosOverride;
+ if (format != null) {
+ if (format.hasLenient()) {
+ leniency = format.getLenient();
+ }
+ if (format.hasPattern()) {
+ final String pattern = format.getPattern();
+ unitConverter = DurationUnitConverter.from(pattern);
+ if (unitConverter == null) {
+ ctxt.reportBadDefinition(getValueType(ctxt),
+ String.format(
+ "Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
+ pattern, DurationUnitConverter.descForAllowed()));
+ }
+ }
+ timestampsAsNanosOverride =
+ format.getFeature(JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+ if (leniency != _isLenient
+ || !Objects.equals(unitConverter, _durationUnitConverter)
+ || !Objects.equals(timestampsAsNanosOverride, _readTimestampsAsNanosOverride)) {
+ return new DurationDeserializer(
+ this, leniency, unitConverter, timestampsAsNanosOverride);
+ }
+ return deser;
+ }
+
+ @Override
+ public Duration deserialize(JsonParser parser, DeserializationContext context)
+ throws JacksonException
+ {
+ switch (parser.currentTokenId())
+ {
+ case JsonTokenId.ID_NUMBER_FLOAT:
+ BigDecimal value = parser.getDecimalValue();
+ // [modules-java8#337] since 2.19, Duration does not need negative adjustment
+ return DecimalUtils.extractSecondsAndNanos(value, Duration::ofSeconds, false);
+ case JsonTokenId.ID_NUMBER_INT:
+ return _fromTimestamp(context, parser.getLongValue());
+ case JsonTokenId.ID_STRING:
+ return _fromString(parser, context, parser.getString());
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ case JsonTokenId.ID_START_OBJECT:
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ case JsonTokenId.ID_EMBEDDED_OBJECT:
+ // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
+ // values quite easily
+ return (Duration) parser.getEmbeddedObject();
+
+ case JsonTokenId.ID_START_ARRAY:
+ return _deserializeFromArray(parser, context);
+ }
+ return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
+ JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
+ }
+
+ protected Duration _fromString(JsonParser parser, DeserializationContext ctxt,
+ String value0)
+ throws JacksonException
+ {
+ String value = value0.trim();
+ if (value.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(parser, ctxt, value);
+ }
+ // 30-Sep-2020: Should allow use of "Timestamp as String" for
+ // some textual formats
+ if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS)
+ && _isValidTimestampString(value)) {
+ return _fromTimestamp(ctxt, NumberInput.parseLong(value));
+ }
+
+ try {
+ return Duration.parse(value);
+ } catch (DateTimeException e) {
+ // null format -> "default formatter"
+ return _handleDateTimeFormatException(ctxt, e, null, value);
+ }
+ }
+
+ protected Duration _fromTimestamp(DeserializationContext ctxt, long ts)
+ {
+ if (_durationUnitConverter != null) {
+ return _durationUnitConverter.convert(ts);
+ }
+ // 20-Oct-2020, tatu: This makes absolutely no sense but... somehow
+ // became the default handling.
+ if (shouldReadTimestampsAsNanoseconds(ctxt)) {
+ return Duration.ofSeconds(ts);
+ }
+ return Duration.ofMillis(ts);
+ }
+
+ protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) {
+ return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride :
+ context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java
new file mode 100644
index 0000000000..e06cdfdb4e
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/InstantDeserializer.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.math.BigDecimal;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.*;
+import tools.jackson.core.io.NumberInput;
+import tools.jackson.databind.BeanProperty;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.cfg.DatatypeFeatures;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+/**
+ * Deserializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime},
+ * and {@link ZonedDateTime}s.
+ *
+ * @author Nick Williams
+ */
+public class InstantDeserializer
+ extends JSR310DateTimeDeserializerBase
+{
+ private final static boolean DEFAULT_NORMALIZE_ZONE_ID = DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID.enabledByDefault();
+ private final static boolean DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
+ = DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS.enabledByDefault();
+
+ /**
+ * Constants used to check if ISO 8601 time string is colon-less. See [jackson-modules-java8#131]
+ */
+ protected static final Pattern ISO8601_COLONLESS_OFFSET_REGEX = Pattern.compile("[+-][0-9]{4}(?=\\[|$)");
+
+ // @since 2.18.2
+ private static OffsetDateTime decimalToOffsetDateTime(FromDecimalArguments args) {
+ // [jackson-modules-java8#308] Since 2.18.2 : Fix can't deserialize OffsetDateTime.MIN: Invalid value for EpochDay
+ if (args.integer == OffsetDateTime.MIN.toEpochSecond() && args.fraction == OffsetDateTime.MIN.getNano()) {
+ return OffsetDateTime.ofInstant(Instant.ofEpochSecond(OffsetDateTime.MIN.toEpochSecond(), OffsetDateTime.MIN.getNano()), OffsetDateTime.MIN.getOffset());
+ }
+ // [jackson-modules-java8#308] Since 2.18.2 : For OffsetDateTime.MAX case
+ if (args.integer == OffsetDateTime.MAX.toEpochSecond() && args.fraction == OffsetDateTime.MAX.getNano()) {
+ return OffsetDateTime.ofInstant(Instant.ofEpochSecond(OffsetDateTime.MAX.toEpochSecond(), OffsetDateTime.MAX.getNano()), OffsetDateTime.MAX.getOffset());
+ }
+ return OffsetDateTime.ofInstant(Instant.ofEpochSecond(args.integer, args.fraction), args.zoneId);
+ }
+
+ public static final InstantDeserializer INSTANT = new InstantDeserializer<>(
+ Instant.class, DateTimeFormatter.ISO_INSTANT,
+ Instant::from,
+ a -> Instant.ofEpochMilli(a.value),
+ a -> Instant.ofEpochSecond(a.integer, a.fraction),
+ null,
+ true, // yes, replace zero offset with Z
+ DEFAULT_NORMALIZE_ZONE_ID,
+ DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
+ );
+
+ public static final InstantDeserializer OFFSET_DATE_TIME = new InstantDeserializer<>(
+ OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME,
+ OffsetDateTime::from,
+ a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
+ InstantDeserializer::decimalToOffsetDateTime,
+ (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))),
+ true, // yes, replace zero offset with Z
+ DEFAULT_NORMALIZE_ZONE_ID,
+ DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
+ );
+
+ public static final InstantDeserializer ZONED_DATE_TIME = new InstantDeserializer<>(
+ ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ ZonedDateTime::from,
+ a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
+ a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
+ ZonedDateTime::withZoneSameInstant,
+ false, // keep zero offset and Z separate since zones explicitly supported
+ DEFAULT_NORMALIZE_ZONE_ID,
+ DEFAULT_ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS
+ );
+
+ protected final Function fromMilliseconds;
+
+ protected final Function fromNanoseconds;
+
+ protected final Function parsedToValue;
+
+ protected final BiFunction adjust;
+
+ /**
+ * In case of vanilla `Instant` we seem to need to translate "+0000 | +00:00 | +00"
+ * timezone designator into plain "Z" for some reason; see
+ * [jackson-modules-java8#18] for more info
+ *
+ * @since 2.9.0
+ */
+ protected final boolean replaceZeroOffsetAsZ;
+
+ /**
+ * Flag for JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE
+ */
+ protected final Boolean _adjustToContextTZOverride;
+
+ /**
+ * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ *
+ * @since 2.16
+ */
+ protected final Boolean _readTimestampsAsNanosOverride;
+
+ /**
+ * Flag set from
+ * {@link DateTimeFeature#NORMALIZE_DESERIALIZED_ZONE_ID} to
+ * determine whether {@link ZoneId} is to be normalized during deserialization.
+ *
+ * @since 2.16
+ */
+ protected final boolean _normalizeZoneId;
+
+ /**
+ * Flag set from
+ * {@link DateTimeFeature#ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS}
+ * to determine whether stringified numbers are interpreted as timestamps
+ * (enabled) nor not (disabled) in addition to a custom pattern ({code DateTimeFormatter}).
+ *
+ * NOTE: stringified timestamps are always allowed with default patterns;
+ * this flag only affects handling of custom patterns.
+ *
+ * @since 2.16
+ */
+ protected final boolean _alwaysAllowStringifiedDateTimestamps;
+
+ /**
+ * @since 2.16
+ */
+ protected InstantDeserializer(Class supportedType,
+ DateTimeFormatter formatter,
+ Function parsedToValue,
+ Function fromMilliseconds,
+ Function fromNanoseconds,
+ BiFunction adjust,
+ boolean replaceZeroOffsetAsZ,
+ boolean normalizeZoneId,
+ boolean readNumericStringsAsTimestamp
+ )
+ {
+ super(supportedType, formatter);
+ this.parsedToValue = parsedToValue;
+ this.fromMilliseconds = fromMilliseconds;
+ this.fromNanoseconds = fromNanoseconds;
+ this.adjust = adjust == null ? ((d, z) -> d) : adjust;
+ this.replaceZeroOffsetAsZ = replaceZeroOffsetAsZ;
+ this._adjustToContextTZOverride = null;
+ this._readTimestampsAsNanosOverride = null;
+ _normalizeZoneId = normalizeZoneId;
+ _alwaysAllowStringifiedDateTimestamps = readNumericStringsAsTimestamp;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f)
+ {
+ super((Class) base.handledType(), f);
+ parsedToValue = base.parsedToValue;
+ fromMilliseconds = base.fromMilliseconds;
+ fromNanoseconds = base.fromNanoseconds;
+ adjust = base.adjust;
+ replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
+ _adjustToContextTZOverride = base._adjustToContextTZOverride;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _normalizeZoneId = base._normalizeZoneId;
+ _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected InstantDeserializer(InstantDeserializer base, Boolean adjustToContextTimezoneOverride)
+ {
+ super((Class) base.handledType(), base._formatter);
+ parsedToValue = base.parsedToValue;
+ fromMilliseconds = base.fromMilliseconds;
+ fromNanoseconds = base.fromNanoseconds;
+ adjust = base.adjust;
+ replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ;
+ _adjustToContextTZOverride = adjustToContextTimezoneOverride;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _normalizeZoneId = base._normalizeZoneId;
+ _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected InstantDeserializer(InstantDeserializer base, DateTimeFormatter f, Boolean leniency)
+ {
+ super((Class) base.handledType(), f, leniency);
+ parsedToValue = base.parsedToValue;
+ fromMilliseconds = base.fromMilliseconds;
+ fromNanoseconds = base.fromNanoseconds;
+ adjust = base.adjust;
+ replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
+ _adjustToContextTZOverride = base._adjustToContextTZOverride;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _normalizeZoneId = base._normalizeZoneId;
+ _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
+ }
+
+ /**
+ * @since 2.16
+ */
+ protected InstantDeserializer(InstantDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape,
+ Boolean adjustToContextTimezoneOverride,
+ Boolean readTimestampsAsNanosOverride)
+ {
+ super(base, leniency, formatter, shape);
+ parsedToValue = base.parsedToValue;
+ fromMilliseconds = base.fromMilliseconds;
+ fromNanoseconds = base.fromNanoseconds;
+ adjust = base.adjust;
+ replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ;
+ _adjustToContextTZOverride = adjustToContextTimezoneOverride;
+ _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ _normalizeZoneId = base._normalizeZoneId;
+ _alwaysAllowStringifiedDateTimestamps = base._alwaysAllowStringifiedDateTimestamps;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected InstantDeserializer(InstantDeserializer base,
+ DatatypeFeatures features)
+ {
+ super((Class) base.handledType(), base._formatter);
+ parsedToValue = base.parsedToValue;
+ fromMilliseconds = base.fromMilliseconds;
+ fromNanoseconds = base.fromNanoseconds;
+ adjust = base.adjust;
+ replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ;
+ _adjustToContextTZOverride = base._adjustToContextTZOverride;
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+
+ _normalizeZoneId = features.isEnabled(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID);
+ _alwaysAllowStringifiedDateTimestamps = features.isEnabled(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS);
+ }
+
+ @Override
+ protected InstantDeserializer withDateFormat(DateTimeFormatter dtf) {
+ if (dtf == _formatter) {
+ return this;
+ }
+ return new InstantDeserializer<>(this, dtf);
+ }
+
+ @Override
+ protected InstantDeserializer withLeniency(Boolean leniency) {
+ return new InstantDeserializer<>(this, _formatter, leniency);
+ }
+
+ public InstantDeserializer withFeatures(DatatypeFeatures features) {
+ if ((_normalizeZoneId == features.isEnabled(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID))
+ && (_alwaysAllowStringifiedDateTimestamps == features.isEnabled(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS))
+ ) {
+ return this;
+ }
+ return new InstantDeserializer<>(this, features);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override // @since 2.12.1
+ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(DeserializationContext ctxt,
+ BeanProperty property, JsonFormat.Value formatOverrides)
+ {
+ InstantDeserializer deser = (InstantDeserializer) super._withFormatOverrides(ctxt,
+ property, formatOverrides);
+ Boolean adjustToContextTZOverride = formatOverrides.getFeature(
+ JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+ Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
+ JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ if (!Objects.equals(adjustToContextTZOverride, deser._adjustToContextTZOverride)
+ || !Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
+ return new InstantDeserializer<>(deser, deser._isLenient, deser._formatter,
+ deser._shape, adjustToContextTZOverride, readTimestampsAsNanosOverride);
+ }
+ return deser;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T deserialize(JsonParser parser, DeserializationContext context)
+ throws JacksonException
+ {
+ //NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only
+ //string values have to be adjusted to the configured TZ.
+ switch (parser.currentTokenId())
+ {
+ case JsonTokenId.ID_NUMBER_FLOAT:
+ return _fromDecimal(context, parser.getDecimalValue());
+ case JsonTokenId.ID_NUMBER_INT:
+ return _fromLong(context, parser.getLongValue());
+ case JsonTokenId.ID_STRING:
+ return _fromString(parser, context, parser.getString());
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ case JsonTokenId.ID_START_OBJECT:
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ case JsonTokenId.ID_EMBEDDED_OBJECT:
+ // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
+ // values quite easily
+ return (T) parser.getEmbeddedObject();
+
+ case JsonTokenId.ID_START_ARRAY:
+ return _deserializeFromArray(parser, context);
+ }
+ return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
+ JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
+ }
+
+ protected boolean shouldAdjustToContextTimezone(DeserializationContext context) {
+ return (_adjustToContextTZOverride != null) ? _adjustToContextTZOverride :
+ context.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+ }
+
+ protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) {
+ return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride :
+ context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ // Helper method to find Strings of form "all digits" and "digits-comma-digits"
+ protected int _countPeriods(String str)
+ {
+ int commas = 0;
+ int i = 0;
+ int ch = str.charAt(i);
+ if (ch == '-' || ch == '+') {
+ ++i;
+ }
+ for (int end = str.length(); i < end; ++i) {
+ ch = str.charAt(i);
+ if (ch < '0' || ch > '9') {
+ if (ch == '.') {
+ ++commas;
+ } else {
+ return -1;
+ }
+ }
+ }
+ return commas;
+ }
+
+ protected T _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ // only check for other parsing modes if we are using default formatter or explicitly asked to
+ if (_alwaysAllowStringifiedDateTimestamps ||
+ _formatter == DateTimeFormatter.ISO_INSTANT ||
+ _formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME ||
+ _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME
+ ) {
+ // 22-Jan-2016, [datatype-jsr310#16]: Allow quoted numbers too
+ int dots = _countPeriods(string);
+ if (dots >= 0) { // negative if not simple number
+ try {
+ if (dots == 0) {
+ return _fromLong(ctxt, NumberInput.parseLong(string));
+ }
+ if (dots == 1) {
+ return _fromDecimal(ctxt, NumberInput.parseBigDecimal(string, false));
+ }
+ } catch (NumberFormatException e) {
+ // fall through to default handling, to get error there
+ }
+ }
+
+ string = replaceZeroOffsetAsZIfNecessary(string);
+ }
+
+ // For some reason DateTimeFormatter.ISO_INSTANT only supports UTC ISO 8601 strings, so it have to be excluded
+ if (_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME ||
+ _formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) {
+
+ // 21-March-2021, Oeystein: Work-around to support basic iso 8601 format (colon-less).
+ // As per JSR-310; Only extended 8601 formats (with colon) are supported for
+ // ZonedDateTime.parse() and OffsetDateTime.parse().
+ // https://github.com/FasterXML/jackson-modules-java8/issues/131
+ string = addInColonToOffsetIfMissing(string);
+ }
+
+ T value;
+ try {
+ TemporalAccessor acc = _formatter.parse(string);
+ value = parsedToValue.apply(acc);
+ if (shouldAdjustToContextTimezone(ctxt)) {
+ return adjust.apply(value, getZone(ctxt));
+ }
+ } catch (DateTimeException e) {
+ value = _handleDateTimeFormatException(ctxt, e, _formatter, string);
+ }
+ return value;
+ }
+
+ protected T _fromLong(DeserializationContext context, long timestamp)
+ {
+ if(shouldReadTimestampsAsNanoseconds(context)){
+ return fromNanoseconds.apply(new FromDecimalArguments(
+ timestamp, 0, this.getZone(context)
+ ));
+ }
+ return fromMilliseconds.apply(new FromIntegerArguments(
+ timestamp, this.getZone(context)));
+ }
+
+ protected T _fromDecimal(DeserializationContext context, BigDecimal value)
+ {
+ FromDecimalArguments args =
+ DecimalUtils.extractSecondsAndNanos(value, (s, ns) -> new FromDecimalArguments(s, ns, getZone(context)),
+ // [modules-java8#337] since 2.19, only Instant needs negative adjustment
+ true);
+ return fromNanoseconds.apply(args);
+ }
+
+ private ZoneId getZone(DeserializationContext context)
+ {
+ // Instants are always in UTC, so don't waste compute cycles
+ // Normalizing the zone to prevent discrepancies.
+ // See https://github.com/FasterXML/jackson-modules-java8/pull/267 for details
+ if (_valueClass == Instant.class) {
+ return null;
+ }
+ ZoneId zoneId = context.getTimeZone().toZoneId();
+ return _normalizeZoneId ? zoneId.normalized() : zoneId;
+ }
+
+ private String replaceZeroOffsetAsZIfNecessary(String text)
+ {
+ if (replaceZeroOffsetAsZ) {
+ return replaceZeroOffsetAsZ(text);
+ }
+
+ return text;
+ }
+
+ private static String replaceZeroOffsetAsZ(String text)
+ {
+ int plusIndex = text.lastIndexOf('+');
+ if (plusIndex < 0) {
+ return text;
+ }
+ int maybeOffsetIndex = plusIndex + 1;
+ int remaining = text.length() - maybeOffsetIndex;
+ switch (remaining) {
+ case 2:
+ return text.regionMatches(maybeOffsetIndex, "00", 0, remaining)
+ ? text.substring(0, plusIndex) + 'Z'
+ : text;
+ case 4:
+ return text.regionMatches(maybeOffsetIndex, "0000", 0, remaining)
+ ? text.substring(0, plusIndex) + 'Z'
+ : text;
+ case 5:
+ return text.regionMatches(maybeOffsetIndex, "00:00", 0, remaining)
+ ? text.substring(0, plusIndex) + 'Z'
+ : text;
+ }
+ return text;
+ }
+
+ // @since 2.13
+ private static String addInColonToOffsetIfMissing(String text)
+ {
+ int timeIndex = text.indexOf('T');
+ if (timeIndex < 0 || timeIndex > text.length() - 1) {
+ return text;
+ }
+
+ int offsetIndex = text.indexOf('+', timeIndex + 1);
+ if (offsetIndex < 0) {
+ offsetIndex = text.indexOf('-', timeIndex + 1);
+ }
+
+ if (offsetIndex < 0 || offsetIndex > text.length() - 5) {
+ return text;
+ }
+
+ int colonIndex = text.indexOf(':', offsetIndex);
+ if (colonIndex == offsetIndex + 3) {
+ return text;
+ }
+
+ if (Character.isDigit(text.charAt(offsetIndex + 1))
+ && Character.isDigit(text.charAt(offsetIndex + 2))
+ && Character.isDigit(text.charAt(offsetIndex + 3))
+ && Character.isDigit(text.charAt(offsetIndex + 4))) {
+ String match = text.substring(offsetIndex, offsetIndex + 5);
+ return text.substring(0, offsetIndex)
+ + match.substring(0, 3) + ':' + match.substring(3)
+ + text.substring(offsetIndex + match.length());
+ }
+
+ // fallback to slow regex path, should be fully handled by the above
+ final Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text);
+ if (matcher.find()) {
+ String match = matcher.group(0);
+ return matcher.replaceFirst(match.substring(0, 3) + ':' + match.substring(3));
+ }
+ return text;
+ }
+
+ public static class FromIntegerArguments // since 2.8.3
+ {
+ public final long value;
+ public final ZoneId zoneId;
+
+ FromIntegerArguments(long value, ZoneId zoneId)
+ {
+ this.value = value;
+ this.zoneId = zoneId;
+ }
+ }
+
+ public static class FromDecimalArguments // since 2.8.3
+ {
+ public final long integer;
+ public final int fraction;
+ public final ZoneId zoneId;
+
+ FromDecimalArguments(long integer, int fraction, ZoneId zoneId)
+ {
+ this.integer = integer;
+ this.fraction = fraction;
+ this.zoneId = zoneId;
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java
new file mode 100644
index 0000000000..2a61db5d65
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DateTimeDeserializerBase.java
@@ -0,0 +1,175 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.ResolverStyle;
+import java.util.Locale;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+import com.fasterxml.jackson.annotation.JsonFormat.Shape;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+
+import tools.jackson.databind.*;
+
+public abstract class JSR310DateTimeDeserializerBase
+ extends JSR310DeserializerBase
+{
+ protected final DateTimeFormatter _formatter;
+
+ /**
+ * Setting that indicates the {@Link JsonFormat.Shape} specified for this deserializer
+ * as a {@link com.fasterxml.jackson.annotation.JsonFormat.Shape} annotation on
+ * property or class, or due to per-type "config override", or from global settings:
+ * If Shape is NUMBER_INT, the input value is considered to be epoch days. If not a
+ * NUMBER_INT, and the deserializer was not specified with the leniency setting of true,
+ * then an exception will be thrown.
+ */
+ protected final Shape _shape;
+
+ protected JSR310DateTimeDeserializerBase(Class supportedType, DateTimeFormatter f) {
+ super(supportedType);
+ _formatter = f;
+ _shape = null;
+ }
+
+ public JSR310DateTimeDeserializerBase(Class supportedType, DateTimeFormatter f, Boolean leniency) {
+ super(supportedType, leniency);
+ _formatter = f;
+ _shape = null;
+ }
+
+ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base,
+ DateTimeFormatter f) {
+ super(base);
+ _formatter = f;
+ _shape = base._shape;
+ }
+
+ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base,
+ Boolean leniency) {
+ super(base, leniency);
+ _formatter = base._formatter;
+ _shape = base._shape;
+ }
+
+ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base,
+ Shape shape) {
+ super(base);
+ _formatter = base._formatter;
+ _shape = shape;
+ }
+
+ /**
+ * @since 2.16
+ */
+ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape) {
+ super(base, leniency);
+ _formatter = formatter;
+ _shape = shape;
+ }
+
+ protected abstract JSR310DateTimeDeserializerBase withDateFormat(DateTimeFormatter dtf);
+
+ @Override
+ protected abstract JSR310DateTimeDeserializerBase withLeniency(Boolean leniency);
+
+ /**
+ * The default implementation returns this, because shape is more likely applicable in case of the serialization,
+ * usage during deserialization could cover only very specific cases.
+ */
+ protected JSR310DateTimeDeserializerBase withShape(Shape shape) {
+ return this;
+ }
+
+ @Override
+ public ValueDeserializer> createContextual(DeserializationContext ctxt,
+ BeanProperty property)
+ {
+ JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
+ return (format == null) ? this : _withFormatOverrides(ctxt, property, format);
+ }
+
+ /**
+ * @param ctxt Active deserialization context
+ * @param property (optional) Property on which this deserializer is used, or {@code null}
+ * for root value
+ * @param formatOverrides Format overrides to use (non-null)
+ *
+ * @return Either this deserializer as is, or newly constructed variant if created
+ * for different configuration
+ *
+ * @since 2.12.1
+ */
+ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(DeserializationContext ctxt,
+ BeanProperty property, JsonFormat.Value formatOverrides)
+ {
+ JSR310DateTimeDeserializerBase> deser = this;
+
+ // 17-Aug-2019, tatu: For 2.10 let's start considering leniency/strictness too
+ if (formatOverrides.hasLenient()) {
+ Boolean leniency = formatOverrides.getLenient();
+ if (leniency != null) {
+ deser = deser.withLeniency(leniency);
+ }
+ }
+ if (formatOverrides.hasPattern()) {
+ final String pattern = formatOverrides.getPattern();
+ final Locale locale = formatOverrides.hasLocale() ? formatOverrides.getLocale() : ctxt.getLocale();
+ DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
+ if (acceptCaseInsensitiveValues(ctxt, formatOverrides)) {
+ builder.parseCaseInsensitive();
+ }
+ builder.appendPattern(pattern);
+ DateTimeFormatter df;
+ if (locale == null) {
+ df = builder.toFormatter();
+ } else {
+ df = builder.toFormatter(locale);
+ }
+
+ // [#148]: allow strict parsing
+ if (!deser.isLenient()) {
+ df = df.withResolverStyle(ResolverStyle.STRICT);
+ }
+
+ // [#69]: For instant serializers/deserializers we need to configure the formatter with
+ //a time zone picked up from JsonFormat annotation, otherwise serialization might not work
+ if (formatOverrides.hasTimeZone()) {
+ df = df.withZone(formatOverrides.getTimeZone().toZoneId());
+ }
+ deser = deser.withDateFormat(df);
+ }
+ // [#58]: For LocalDate deserializers we need to configure the formatter with
+ //a shape picked up from JsonFormat annotation, to decide if the value is EpochSeconds
+ JsonFormat.Shape shape = formatOverrides.getShape();
+ if (shape != null && shape != _shape) {
+ deser = deser.withShape(shape);
+ }
+ // any use for TimeZone?
+
+ return deser;
+ }
+
+ private boolean acceptCaseInsensitiveValues(DeserializationContext ctxt, JsonFormat.Value format)
+ {
+ Boolean enabled = format.getFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES);
+ if (enabled == null) {
+ enabled = ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
+ }
+ return enabled;
+ }
+
+ protected void _throwNoNumericTimestampNeedTimeZone(JsonParser p, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ ctxt.reportInputMismatch(handledType(),
+"raw timestamp (%d) not allowed for `%s`: need additional information such as an offset or time-zone (see class Javadocs)",
+p.getNumberValue(), handledType().getName());
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java
new file mode 100644
index 0000000000..23ca16f384
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310DeserializerBase.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.io.NumberInput;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.deser.std.StdScalarDeserializer;
+import tools.jackson.databind.jsontype.TypeDeserializer;
+import tools.jackson.databind.type.LogicalType;
+import tools.jackson.databind.util.ClassUtil;
+
+/**
+ * Base class that indicates that all JSR310 datatypes are deserialized from scalar JSON types.
+ *
+ * @author Nick Williams
+ */
+abstract class JSR310DeserializerBase extends StdScalarDeserializer
+{
+ /**
+ * Flag that indicates what leniency setting is enabled for this deserializer (either
+ * due {@link com.fasterxml.jackson.annotation.JsonFormat.Shape} annotation on property or class, or due to per-type
+ * "config override", or from global settings): leniency/strictness has effect
+ * on accepting some non-default input value representations (such as integer values
+ * for dates).
+ *
+ * Note that global default setting is for leniency to be enabled, for Jackson 2.x,
+ * and has to be explicitly change to force strict handling: this is to keep backwards
+ * compatibility with earlier versions.
+ */
+ protected final boolean _isLenient;
+
+ protected JSR310DeserializerBase(Class supportedType) {
+ super(supportedType);
+ _isLenient = true;
+ }
+
+ protected JSR310DeserializerBase(Class supportedType,
+ Boolean leniency) {
+ super(supportedType);
+ _isLenient = !Boolean.FALSE.equals(leniency);
+ }
+
+ protected JSR310DeserializerBase(JSR310DeserializerBase base) {
+ super(base);
+ _isLenient = base._isLenient;
+ }
+
+ protected JSR310DeserializerBase(JSR310DeserializerBase base, Boolean leniency) {
+ super(base);
+ _isLenient = !Boolean.FALSE.equals(leniency);
+ }
+
+ protected abstract JSR310DeserializerBase withLeniency(Boolean leniency);
+
+ /**
+ * @return {@code true} if lenient handling is enabled; {code false} if not (strict mode)
+ */
+ protected boolean isLenient() {
+ return _isLenient;
+ }
+
+ /**
+ * Replacement for {@code isLenient()} for specific case of deserialization
+ * from empty or blank String.
+ *
+ * @since 2.12
+ */
+ @SuppressWarnings("unchecked")
+ protected T _fromEmptyString(JsonParser p, DeserializationContext ctxt,
+ String str)
+ throws JacksonException
+ {
+ final CoercionAction act = _checkFromStringCoercion(ctxt, str);
+ switch (act) { // note: Fail handled above
+ case AsEmpty:
+ return (T) getEmptyValue(ctxt);
+ case TryConvert:
+ case AsNull:
+ default:
+ }
+ // 22-Oct-2020, tatu: Although we should probably just accept this,
+ // for backwards compatibility let's for now allow override by
+ // "Strict" checks
+ if (!_isLenient) {
+ return _failForNotLenient(p, ctxt, JsonToken.VALUE_STRING);
+ }
+
+ return null;
+ }
+
+ // Presumably all types here are Date/Time oriented ones?
+ @Override
+ public LogicalType logicalType() { return LogicalType.DateTime; }
+
+ @Override
+ public Object deserializeWithType(JsonParser parser, DeserializationContext context,
+ TypeDeserializer typeDeserializer)
+ throws JacksonException
+ {
+ return typeDeserializer.deserializeTypedFromAny(parser, context);
+ }
+
+ // @since 2.12
+ protected boolean _isValidTimestampString(String str) {
+ // 30-Sep-2020, tatu: Need to support "numbers as Strings" for data formats
+ // that only have String values for scalars (CSV, Properties, XML)
+ // NOTE: we do allow negative values, but has to fit in 64-bits:
+ return _isIntNumber(str) && NumberInput.inLongRange(str, (str.charAt(0) == '-'));
+ }
+
+ protected BOGUS _reportWrongToken(DeserializationContext context,
+ JsonToken exp, String unit)
+ throws JacksonException
+ {
+ context.reportWrongTokenException((ValueDeserializer>)this, exp,
+ "Expected %s for '%s' of %s value",
+ exp.name(), unit, ClassUtil.getClassDescription(handledType()));
+ return null;
+ }
+
+ protected BOGUS _reportWrongToken(JsonParser parser, DeserializationContext context,
+ JsonToken... expTypes)
+ throws JacksonException
+ {
+ // 20-Apr-2016, tatu: No multiple-expected-types handler yet, construct message here
+ return context.reportInputMismatch(handledType(),
+ "Unexpected token (%s), expected one of %s for %s value",
+ parser.currentToken(),
+ Arrays.asList(expTypes).toString(),
+ ClassUtil.getClassDescription(handledType()));
+ }
+
+ @SuppressWarnings("unchecked")
+ protected R _handleDateTimeException(DeserializationContext context,
+ DateTimeException e0, String value)
+ throws JacksonException
+ {
+ try {
+ return (R) context.handleWeirdStringValue(handledType(), value,
+ "Failed to deserialize %s: (%s) %s",
+ ClassUtil.getClassDescription(handledType()), e0.getClass().getName(), e0.getMessage());
+ } catch (JacksonException e) {
+ e.initCause(e0);
+ throw e;
+ }
+ }
+
+ // @since 3.0
+ @SuppressWarnings("unchecked")
+ protected R _handleDateTimeFormatException(DeserializationContext context,
+ DateTimeException e0, DateTimeFormatter format, String value)
+ throws JacksonException
+ {
+ final String formatterDesc = (format == null) ? "[default format]" : format.toString();
+ try {
+ return (R) context.handleWeirdStringValue(handledType(), value,
+ "Failed to deserialize %s (with format '%s'): (%s) %s",
+ ClassUtil.getClassDescription(handledType()),
+ formatterDesc, e0.getClass().getName(), e0.getMessage());
+ } catch (JacksonException e) {
+ e.initCause(e0);
+ throw e;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected R _handleUnexpectedToken(DeserializationContext ctxt,
+ JsonParser parser, String message, Object... args)
+ {
+ return (R) ctxt.handleUnexpectedToken(getValueType(ctxt), parser.currentToken(),
+ parser, message, args);
+ }
+
+ protected R _handleUnexpectedToken(DeserializationContext context,
+ JsonParser parser, JsonToken... expTypes)
+ {
+ return _handleUnexpectedToken(context, parser,
+ "Unexpected token (%s), expected one of %s for %s value",
+ parser.currentToken(),
+ Arrays.asList(expTypes),
+ ClassUtil.getClassDescription(handledType()));
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T _failForNotLenient(JsonParser p, DeserializationContext ctxt,
+ JsonToken expToken)
+ throws JacksonException
+ {
+ return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), expToken, p,
+ "Cannot deserialize instance of %s out of %s token: not allowed because 'strict' mode set for property or type (enable 'lenient' handling to allow)",
+ ClassUtil.nameOf(handledType()), p.currentToken());
+ }
+
+ /*
+ public Object handleUnexpectedToken(Class> instClass, JsonToken t,
+ JsonParser p, String msg, Object... msgArgs)
+ */
+
+ /**
+ * Helper method used to peel off spurious wrappings of DateTimeException
+ *
+ * @param e DateTimeException to peel
+ *
+ * @return DateTimeException that does not have another DateTimeException as its cause.
+ */
+ protected DateTimeException _peelDTE(DateTimeException e) {
+ while (true) {
+ Throwable t = e.getCause();
+ if (t != null && t instanceof DateTimeException) {
+ e = (DateTimeException) t;
+ continue;
+ }
+ break;
+ }
+ return e;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java
new file mode 100644
index 0000000000..2cb31ed3ea
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JSR310StringParsableDeserializer.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+
+import tools.jackson.core.JsonToken;
+
+import tools.jackson.core.util.VersionUtil;
+
+import tools.jackson.databind.BeanProperty;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.ValueDeserializer;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.jsontype.TypeDeserializer;
+
+/**
+ * Deserializer for all Java 8 temporal {@link java.time} types that cannot be represented
+ * with numbers and that have parse functions that can take {@link String}s,
+ * and where format is not configurable.
+ *
+ * @author Nick Williams
+ * @author Tatu Saloranta
+ */
+public class JSR310StringParsableDeserializer
+ extends JSR310DeserializerBase
+{
+ protected final static int TYPE_PERIOD = 1;
+ protected final static int TYPE_ZONE_ID = 2;
+ protected final static int TYPE_ZONE_OFFSET = 3;
+
+ public static final ValueDeserializer PERIOD =
+ createDeserializer(Period.class, TYPE_PERIOD);
+
+ public static final ValueDeserializer ZONE_ID =
+ createDeserializer(ZoneId.class, TYPE_ZONE_ID);
+
+ public static final ValueDeserializer ZONE_OFFSET =
+ createDeserializer(ZoneOffset.class, TYPE_ZONE_OFFSET);
+
+ protected final int _typeSelector;
+
+ @SuppressWarnings("unchecked")
+ protected JSR310StringParsableDeserializer(Class> supportedType, int typeSelector)
+ {
+ super((Class)supportedType);
+ _typeSelector = typeSelector;
+ }
+
+ protected JSR310StringParsableDeserializer(JSR310StringParsableDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _typeSelector = base._typeSelector;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static ValueDeserializer createDeserializer(Class type, int typeId) {
+ return (ValueDeserializer) new JSR310StringParsableDeserializer(type, typeId);
+ }
+
+ @Override
+ protected JSR310StringParsableDeserializer withLeniency(Boolean leniency) {
+ if (_isLenient == !Boolean.FALSE.equals(leniency)) {
+ return this;
+ }
+ // TODO: or should this be casting as above in createDeserializer? But then in createContext, we need to
+ // call the withLeniency method in this class. (See if we can follow InstantDeser convention here?)
+ return new JSR310StringParsableDeserializer(this, leniency);
+ }
+
+ @Override
+ public ValueDeserializer> createContextual(DeserializationContext ctxt,
+ BeanProperty property)
+ {
+ JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
+ JSR310StringParsableDeserializer deser = this;
+ if (format != null) {
+ if (format.hasLenient()) {
+ Boolean leniency = format.getLenient();
+ if (leniency != null) {
+ deser = this.withLeniency(leniency);
+ }
+ }
+ }
+ return deser;
+ }
+
+ @Override
+ public Object deserialize(JsonParser p, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ if (p.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(p, ctxt, p.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (p.isExpectedStartObjectToken()) {
+ return _fromString(p, ctxt,
+ ctxt.extractScalarFromObject(p, this, handledType()));
+ }
+ if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded
+ // values quite easily
+ return p.getEmbeddedObject();
+ }
+ if (p.isExpectedStartArrayToken()) {
+ return _deserializeFromArray(p, ctxt);
+ }
+
+ throw ctxt.wrongTokenException(p, handledType(), JsonToken.VALUE_STRING, null);
+ }
+
+ @Override
+ public Object deserializeWithType(JsonParser p, DeserializationContext context,
+ TypeDeserializer deserializer)
+ throws JacksonException
+ {
+ // This is a nasty kludge right here, working around issues like
+ // [datatype-jsr310#24]. But should work better than not having the work-around.
+ JsonToken t = p.currentToken();
+ if ((t != null) && t.isScalarValue()) {
+ return deserialize(p, context);
+ }
+ return deserializer.deserializeTypedFromAny(p, context);
+ }
+
+ protected Object _fromString(JsonParser p, DeserializationContext ctxt,
+ String string)
+ throws JacksonException
+ {
+ string = string.trim();
+ if (string.length() == 0) {
+ CoercionAction act = ctxt.findCoercionAction(logicalType(), _valueClass,
+ CoercionInputShape.EmptyString);
+ if (act == CoercionAction.Fail) {
+ ctxt.reportInputMismatch(this,
+"Cannot coerce empty String (\"\") to %s (but could if enabling coercion using `CoercionConfig`)",
+_coercedTypeDesc());
+ }
+ // 21-Jun-2020, tatu: As of 2.12, leniency considered legacy setting,
+ // but still supported.
+ if (!isLenient()) {
+ return _failForNotLenient(p, ctxt, JsonToken.VALUE_STRING);
+ }
+ if (act == CoercionAction.AsEmpty) {
+ return getEmptyValue(ctxt);
+ }
+ // None of the types has specific null value
+ return null;
+ }
+ try {
+ switch (_typeSelector) {
+ case TYPE_PERIOD:
+ return Period.parse(string);
+ case TYPE_ZONE_ID:
+ return ZoneId.of(string);
+ case TYPE_ZONE_OFFSET:
+ return ZoneOffset.of(string);
+ }
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, null, string);
+ }
+ VersionUtil.throwInternal();
+ return null;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java
new file mode 100644
index 0000000000..7976888128
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/JavaTimeDeserializerModifier.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Month;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.deser.ValueDeserializerModifier;
+
+/**
+ * @since 2.17
+ */
+public class JavaTimeDeserializerModifier extends ValueDeserializerModifier {
+ private static final long serialVersionUID = 1L;
+
+ private final boolean _oneBaseMonths;
+
+ public JavaTimeDeserializerModifier(boolean oneBaseMonths) {
+ _oneBaseMonths = oneBaseMonths;
+ }
+
+ @Override
+ public ValueDeserializer> modifyEnumDeserializer(DeserializationConfig config, JavaType type,
+ BeanDescription beanDesc, ValueDeserializer> defaultDeserializer) {
+ if (_oneBaseMonths && type.hasRawClass(Month.class)) {
+ return new OneBasedMonthDeserializer(defaultDeserializer);
+ }
+ return defaultDeserializer;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java
new file mode 100644
index 0000000000..06a08d466b
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserializer.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.*;
+
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.cfg.DatatypeFeatures;
+import tools.jackson.databind.cfg.DateTimeFeature;
+
+/**
+ * Deserializer for Java 8 temporal {@link LocalDate}s.
+ *
+ * @author Nick Williams
+ */
+public class LocalDateDeserializer extends JSR310DateTimeDeserializerBase
+{
+ private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING
+ = DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault();
+
+ private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
+
+ public static final LocalDateDeserializer INSTANCE = new LocalDateDeserializer();
+
+ /**
+ * Flag set from
+ * {@link DateTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING}
+ * to determine whether the {@link java.util.TimeZone} of the
+ * {@link tools.jackson.databind.DeserializationContext} is used
+ * when leniently deserializing from the UTC/ISO instant format.
+ */
+ protected final boolean _useTimeZoneForLenientDateParsing;
+
+ protected LocalDateDeserializer() {
+ this(DEFAULT_FORMATTER);
+ }
+
+ public LocalDateDeserializer(DateTimeFormatter dtf) {
+ super(LocalDate.class, dtf);
+ _useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING;
+ }
+
+ public LocalDateDeserializer(LocalDateDeserializer base, DateTimeFormatter dtf) {
+ super(base, dtf);
+ _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
+ }
+
+ protected LocalDateDeserializer(LocalDateDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
+ }
+
+ protected LocalDateDeserializer(LocalDateDeserializer base, JsonFormat.Shape shape) {
+ super(base, shape);
+ _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
+ }
+
+ /**
+ * Since 2.19
+ */
+ protected LocalDateDeserializer(LocalDateDeserializer base, DatatypeFeatures features) {
+ super(LocalDate.class, base._formatter);
+ _useTimeZoneForLenientDateParsing = features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING);
+ }
+
+ @Override
+ protected LocalDateDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new LocalDateDeserializer(this, dtf);
+ }
+
+ @Override
+ protected LocalDateDeserializer withLeniency(Boolean leniency) {
+ return new LocalDateDeserializer(this, leniency);
+ }
+
+ @Override
+ protected LocalDateDeserializer withShape(JsonFormat.Shape shape) { return new LocalDateDeserializer(this, shape); }
+
+ /**
+ * Since 2.19
+ */
+ public LocalDateDeserializer withFeatures(DatatypeFeatures features) {
+ if (_useTimeZoneForLenientDateParsing ==
+ features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) {
+ return this;
+ }
+ return new LocalDateDeserializer(this, features);
+ }
+
+ @Override
+ public LocalDate deserialize(JsonParser parser, DeserializationContext context)
+ throws JacksonException
+ {
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (parser.isExpectedStartArrayToken()) {
+ JsonToken t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if (context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ && (t == JsonToken.VALUE_STRING || t==JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ final LocalDate parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ int year = parser.getIntValue();
+ int month = parser.nextIntValue(-1);
+ int day = parser.nextIntValue(-1);
+
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
+ "Expected array to end");
+ }
+ return LocalDate.of(year, month, day);
+ }
+ context.reportInputMismatch(handledType(),
+ "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
+ t);
+ }
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (LocalDate) parser.getEmbeddedObject();
+ }
+ // 06-Jan-2018, tatu: Is this actually safe? Do users expect such coercion?
+ if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ CoercionAction act = context.findCoercionAction(logicalType(), _valueClass,
+ CoercionInputShape.Integer);
+ _checkCoercionFail(context, act, handledType(), parser.getLongValue(),
+ "Integer value (" + parser.getLongValue() + ")");
+
+ // issue 58 - also check for NUMBER_INT, which needs to be specified when serializing.
+ if (_shape == JsonFormat.Shape.NUMBER_INT || isLenient()) {
+ return LocalDate.ofEpochDay(parser.getLongValue());
+ }
+ return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
+ }
+ return _handleUnexpectedToken(context, parser, "Expected array or string.");
+ }
+
+ protected LocalDate _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ // as per [datatype-jsr310#37], only check for optional (and, incorrect...) time marker 'T'
+ // if we are using default formatter
+ final DateTimeFormatter format = _formatter;
+ try {
+ if (format == DEFAULT_FORMATTER) {
+ // JavaScript by default includes time in JSON serialized Dates (UTC/ISO instant format).
+ if (string.length() > 10 && string.charAt(10) == 'T') {
+ if (isLenient()) {
+ if (string.endsWith("Z")) {
+ if (_useTimeZoneForLenientDateParsing) {
+ return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDate();
+ }
+ return LocalDate.parse(string.substring(0, string.length() - 1),
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ }
+ return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ }
+ JavaType t = getValueType(ctxt);
+ return (LocalDate) ctxt.handleWeirdStringValue(t.getRawClass(),
+ string,
+"Should not contain time component when 'strict' mode set for property or type (enable 'lenient' handling to allow)"
+ );
+ }
+ }
+ return LocalDate.parse(string, format);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, format, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java
new file mode 100644
index 0000000000..ead83305d6
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserializer.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.*;
+import tools.jackson.databind.*;
+import tools.jackson.databind.cfg.DatatypeFeatures;
+import tools.jackson.databind.cfg.DateTimeFeature;
+
+/**
+ * Deserializer for Java 8 temporal {@link LocalDateTime}s.
+ *
+ * @author Nick Williams
+ */
+public class LocalDateTimeDeserializer
+ extends JSR310DateTimeDeserializerBase
+{
+ private final static boolean DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING
+ = DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING.enabledByDefault();
+
+ private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+ public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
+
+ /**
+ * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ *
+ * @since 2.16
+ */
+ protected final Boolean _readTimestampsAsNanosOverride;
+
+ /**
+ * Flag set from
+ * {@link DateTimeFeature#USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING}
+ * to determine whether the {@link java.util.TimeZone} of the
+ * {@link tools.jackson.databind.DeserializationContext} is used
+ * when leniently deserializing from the UTC/ISO instant format.
+ *
+ * @since 2.19
+ */
+ protected final boolean _useTimeZoneForLenientDateParsing;
+
+ protected LocalDateTimeDeserializer() { // was private before 2.12
+ this(DEFAULT_FORMATTER);
+ }
+
+ public LocalDateTimeDeserializer(DateTimeFormatter formatter) {
+ super(LocalDateTime.class, formatter);
+ _readTimestampsAsNanosOverride = null;
+ _useTimeZoneForLenientDateParsing = DEFAULT_USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING;
+ }
+
+ /**
+ * Since 2.10
+ */
+ protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
+ }
+
+ /**
+ * Since 2.16
+ */
+ protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape,
+ Boolean readTimestampsAsNanosOverride) {
+ super(base, leniency, formatter, shape);
+ _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ _useTimeZoneForLenientDateParsing = base._useTimeZoneForLenientDateParsing;
+ }
+
+ /**
+ * Since 2.19
+ */
+ protected LocalDateTimeDeserializer(LocalDateTimeDeserializer base, DatatypeFeatures features) {
+ super(LocalDateTime.class, base._formatter);
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ _useTimeZoneForLenientDateParsing = features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING);
+ }
+
+ @Override
+ protected LocalDateTimeDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new LocalDateTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride);
+ }
+
+ @Override
+ protected LocalDateTimeDeserializer withLeniency(Boolean leniency) {
+ return new LocalDateTimeDeserializer(this, leniency);
+ }
+
+ @Override
+ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(DeserializationContext ctxt,
+ BeanProperty property, JsonFormat.Value formatOverrides)
+ {
+ LocalDateTimeDeserializer deser = (LocalDateTimeDeserializer)
+ super._withFormatOverrides(ctxt, property, formatOverrides);
+ Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
+ JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
+ return new LocalDateTimeDeserializer(deser, deser._isLenient, deser._formatter,
+ deser._shape, readTimestampsAsNanosOverride);
+ }
+ return deser;
+ }
+
+ /**
+ * Since 2.19
+ */
+ public LocalDateTimeDeserializer withFeatures(DatatypeFeatures features) {
+ if (_useTimeZoneForLenientDateParsing ==
+ features.isEnabled(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)) {
+ return this;
+ }
+ return new LocalDateTimeDeserializer(this, features);
+ }
+
+ @Override
+ public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws JacksonException
+ {
+ if (parser.hasTokenId(JsonTokenId.ID_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (parser.isExpectedStartArrayToken()) {
+ JsonToken t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
+ && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
+ final LocalDateTime parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ LocalDateTime result;
+
+ int year = parser.getIntValue();
+ int month = parser.nextIntValue(-1);
+ int day = parser.nextIntValue(-1);
+ int hour = parser.nextIntValue(-1);
+ int minute = parser.nextIntValue(-1);
+
+ t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ result = LocalDateTime.of(year, month, day, hour, minute);
+ } else {
+ int second = parser.getIntValue();
+ t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ result = LocalDateTime.of(year, month, day, hour, minute, second);
+ } else {
+ int partialSecond = parser.getIntValue();
+ if (partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context))
+ partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
+ "Expected array to end");
+ }
+ result = LocalDateTime.of(year, month, day, hour, minute, second, partialSecond);
+ }
+ }
+ return result;
+ }
+ context.reportInputMismatch(handledType(),
+ "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
+ t);
+ }
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (LocalDateTime) parser.getEmbeddedObject();
+ }
+ if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ _throwNoNumericTimestampNeedTimeZone(parser, context);
+ }
+ return _handleUnexpectedToken(context, parser, "Expected array or string.");
+ }
+
+ protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) {
+ return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride :
+ context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ protected LocalDateTime _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ final DateTimeFormatter format = _formatter;
+ try {
+ // 21-Oct-2020, tatu: Changed as per [modules-base#94] for 2.12,
+ // had bad timezone handle change from [modules-base#56]
+ if (_formatter == DEFAULT_FORMATTER) {
+ // ... only allow iff lenient mode enabled since
+ // JavaScript by default includes time and zone in JSON serialized Dates (UTC/ISO instant format).
+ if (string.length() > 10 && string.charAt(10) == 'T') {
+ if (string.endsWith("Z")) {
+ if (isLenient()) {
+ if (_useTimeZoneForLenientDateParsing) {
+ return Instant.parse(string).atZone(ctxt.getTimeZone().toZoneId()).toLocalDateTime();
+ }
+ return LocalDateTime.parse(string.substring(0, string.length()-1),
+ _formatter);
+ }
+ JavaType t = getValueType(ctxt);
+ return (LocalDateTime) ctxt.handleWeirdStringValue(t.getRawClass(),
+ string,
+"Should not contain offset when 'strict' mode set for property or type (enable 'lenient' handling to allow)"
+ );
+ }
+ }
+ }
+ return LocalDateTime.parse(string, _formatter);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, format, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java
new file mode 100644
index 0000000000..131a1ff569
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserializer.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.databind.BeanProperty;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+
+/**
+ * Deserializer for Java 8 temporal {@link LocalTime}s.
+ *
+ * @author Nick Williams
+ */
+public class LocalTimeDeserializer extends JSR310DateTimeDeserializerBase
+{
+ private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_TIME;
+
+ public static final LocalTimeDeserializer INSTANCE = new LocalTimeDeserializer();
+
+ /**
+ * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ *
+ * @since 2.16
+ */
+ protected final Boolean _readTimestampsAsNanosOverride;
+
+ protected LocalTimeDeserializer() { // was private before 2.12
+ this(DEFAULT_FORMATTER);
+ }
+
+ public LocalTimeDeserializer(DateTimeFormatter formatter) {
+ super(LocalTime.class, formatter);
+ _readTimestampsAsNanosOverride = null;
+ }
+
+ protected LocalTimeDeserializer(LocalTimeDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ }
+
+ /**
+ * Since 2.16
+ */
+ protected LocalTimeDeserializer(LocalTimeDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape,
+ Boolean readTimestampsAsNanosOverride) {
+ super(base, leniency, formatter, shape);
+ _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ }
+
+ @Override
+ protected LocalTimeDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new LocalTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride);
+ }
+
+ @Override
+ protected LocalTimeDeserializer withLeniency(Boolean leniency) {
+ return new LocalTimeDeserializer(this, leniency);
+ }
+
+ @Override
+ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(DeserializationContext ctxt,
+ BeanProperty property, JsonFormat.Value formatOverrides)
+ {
+ LocalTimeDeserializer deser = (LocalTimeDeserializer)
+ super._withFormatOverrides(ctxt, property, formatOverrides);
+ Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
+ JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
+ return new LocalTimeDeserializer(deser, deser._isLenient, deser._formatter,
+ deser._shape, readTimestampsAsNanosOverride);
+ }
+ return deser;
+ }
+
+ @Override
+ public LocalTime deserialize(JsonParser parser, DeserializationContext context) throws JacksonException
+ {
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (parser.isExpectedStartArrayToken()) {
+ JsonToken t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if (context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ && (t == JsonToken.VALUE_STRING || t==JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ final LocalTime parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ int hour = parser.getIntValue();
+
+ parser.nextToken();
+ int minute = parser.getIntValue();
+ LocalTime result;
+
+ t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ result = LocalTime.of(hour, minute);
+ } else {
+ int second = parser.getIntValue();
+ t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ result = LocalTime.of(hour, minute, second);
+ } else {
+ int partialSecond = parser.getIntValue();
+ if(partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context))
+ partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds
+ t = parser.nextToken();
+ if (t != JsonToken.END_ARRAY) {
+ throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
+ "Expected array to end");
+ }
+ result = LocalTime.of(hour, minute, second, partialSecond);
+ }
+ }
+ return result;
+ }
+ context.reportInputMismatch(handledType(),
+ "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
+ t);
+ }
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (LocalTime) parser.getEmbeddedObject();
+ }
+ if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ _throwNoNumericTimestampNeedTimeZone(parser, context);
+ }
+ return _handleUnexpectedToken(context, parser, "Expected array or string.");
+ }
+
+ protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) {
+ return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride :
+ context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ protected LocalTime _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ final DateTimeFormatter format = _formatter;
+ try {
+ if (format == DEFAULT_FORMATTER) {
+ if (string.contains("T")) {
+ return LocalTime.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ }
+ }
+ return LocalTime.parse(string, format);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, format, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java
new file mode 100644
index 0000000000..9f7b3df94b
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserializer.java
@@ -0,0 +1,129 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.MonthDay;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+
+/**
+ * Deserializer for Java 8 temporal {@link MonthDay}s.
+ */
+public class MonthDayDeserializer extends JSR310DateTimeDeserializerBase
+{
+ public static final MonthDayDeserializer INSTANCE = new MonthDayDeserializer();
+
+ /**
+ * NOTE: only {@code public} so that use via annotations (see [modules-java8#202])
+ * is possible
+ */
+ public MonthDayDeserializer() {
+ this(null);
+ }
+
+ public MonthDayDeserializer(DateTimeFormatter formatter) {
+ super(MonthDay.class, formatter);
+ }
+
+ /**
+ * Since 2.12
+ */
+ protected MonthDayDeserializer(MonthDayDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ }
+
+ /**
+ * Since 2.16
+ */
+ protected MonthDayDeserializer(MonthDayDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape) {
+ super(base, leniency, formatter, shape);
+ }
+
+ @Override
+ protected MonthDayDeserializer withLeniency(Boolean leniency) {
+ return new MonthDayDeserializer(this, leniency);
+ }
+
+ @Override
+ protected MonthDayDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new MonthDayDeserializer(this, _isLenient, dtf, _shape);
+ }
+
+ @Override
+ public MonthDay deserialize(JsonParser parser, DeserializationContext context)
+ throws JacksonException
+ {
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (parser.isExpectedStartArrayToken()) {
+ JsonToken t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
+ && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
+ final MonthDay parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ if (t != JsonToken.VALUE_NUMBER_INT) {
+ _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "month");
+ }
+ int month = parser.getIntValue();
+ int day = parser.nextIntValue(-1);
+ if (day == -1) {
+ if (!parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "day");
+ }
+ day = parser.getIntValue();
+ }
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
+ "Expected array to end");
+ }
+ return MonthDay.of(month, day);
+ }
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (MonthDay) parser.getEmbeddedObject();
+ }
+ return _handleUnexpectedToken(context, parser,
+ JsonToken.VALUE_STRING, JsonToken.START_ARRAY);
+ }
+
+ protected MonthDay _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ try {
+ if (_formatter == null) {
+ return MonthDay.parse(string);
+ }
+ return MonthDay.parse(string, _formatter);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, _formatter, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java
new file mode 100644
index 0000000000..436f633b51
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserializer.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.*;
+import tools.jackson.databind.*;
+
+/**
+ * Deserializer for Java 8 temporal {@link OffsetTime}s.
+ *
+ * @author Nick Williams
+ */
+public class OffsetTimeDeserializer extends JSR310DateTimeDeserializerBase
+{
+ public static final OffsetTimeDeserializer INSTANCE = new OffsetTimeDeserializer();
+
+ /**
+ * Flag for JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ *
+ * @since 2.16
+ */
+ protected final Boolean _readTimestampsAsNanosOverride;
+
+ protected OffsetTimeDeserializer() { // was private before 2.12
+ this(DateTimeFormatter.ISO_OFFSET_TIME);
+ }
+
+ protected OffsetTimeDeserializer(DateTimeFormatter dtf) {
+ super(OffsetTime.class, dtf);
+ _readTimestampsAsNanosOverride = null;
+ }
+
+ /**
+ * Since 2.11
+ */
+ protected OffsetTimeDeserializer(OffsetTimeDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ _readTimestampsAsNanosOverride = base._readTimestampsAsNanosOverride;
+ }
+
+ /**
+ * Since 2.16
+ */
+ protected OffsetTimeDeserializer(OffsetTimeDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape,
+ Boolean readTimestampsAsNanosOverride) {
+ super(base, leniency, formatter, shape);
+ _readTimestampsAsNanosOverride = readTimestampsAsNanosOverride;
+ }
+
+ @Override
+ protected OffsetTimeDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new OffsetTimeDeserializer(this, _isLenient, dtf, _shape, _readTimestampsAsNanosOverride);
+ }
+
+ @Override
+ protected OffsetTimeDeserializer withLeniency(Boolean leniency) {
+ return new OffsetTimeDeserializer(this, leniency);
+ }
+
+ @Override
+ protected JSR310DateTimeDeserializerBase> _withFormatOverrides(DeserializationContext ctxt,
+ BeanProperty property, JsonFormat.Value formatOverrides)
+ {
+ OffsetTimeDeserializer deser = (OffsetTimeDeserializer)
+ super._withFormatOverrides(ctxt, property, formatOverrides);
+ Boolean readTimestampsAsNanosOverride = formatOverrides.getFeature(
+ JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ if (!Objects.equals(readTimestampsAsNanosOverride, deser._readTimestampsAsNanosOverride)) {
+ return new OffsetTimeDeserializer(deser, deser._isLenient, deser._formatter,
+ deser._shape, readTimestampsAsNanosOverride);
+ }
+ return deser;
+ }
+
+ @Override
+ public OffsetTime deserialize(JsonParser parser, DeserializationContext context)
+ throws JacksonException
+ {
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (!parser.isExpectedStartArrayToken()) {
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (OffsetTime) parser.getEmbeddedObject();
+ }
+ if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ _throwNoNumericTimestampNeedTimeZone(parser, context);
+ }
+ throw context.wrongTokenException(parser, handledType(), JsonToken.START_ARRAY,
+ "Expected array or string.");
+ }
+ JsonToken t = parser.nextToken();
+ if (t != JsonToken.VALUE_NUMBER_INT) {
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
+ && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
+ final OffsetTime parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ context.reportInputMismatch(handledType(),
+ "Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
+ t);
+ }
+ int hour = parser.getIntValue();
+ int minute = parser.nextIntValue(-1);
+ if (minute == -1) {
+ t = parser.currentToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if (t != JsonToken.VALUE_NUMBER_INT) {
+ _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "minutes");
+ }
+ minute = parser.getIntValue();
+ }
+ int partialSecond = 0;
+ int second = 0;
+ if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) {
+ second = parser.getIntValue();
+ if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) {
+ partialSecond = parser.getIntValue();
+ if (partialSecond < 1_000 && !shouldReadTimestampsAsNanoseconds(context)) {
+ partialSecond *= 1_000_000; // value is milliseconds, convert it to nanoseconds
+ }
+ parser.nextToken();
+ }
+ }
+ if (parser.currentToken() == JsonToken.VALUE_STRING) {
+ OffsetTime result = OffsetTime.of(hour, minute, second, partialSecond, ZoneOffset.of(parser.getString()));
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ _reportWrongToken(context, JsonToken.END_ARRAY, "timezone");
+ }
+ return result;
+ }
+ throw context.wrongTokenException(parser, handledType(), JsonToken.VALUE_STRING,
+ "Expected string for TimeZone after numeric values");
+ }
+
+ protected boolean shouldReadTimestampsAsNanoseconds(DeserializationContext context) {
+ return (_readTimestampsAsNanosOverride != null) ? _readTimestampsAsNanosOverride :
+ context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ protected OffsetTime _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ try {
+ return OffsetTime.parse(string, _formatter);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, _formatter, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java
new file mode 100644
index 0000000000..6509c72423
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserializer.java
@@ -0,0 +1,71 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Month;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.deser.std.DelegatingDeserializer;
+import tools.jackson.databind.exc.InvalidFormatException;
+
+public class OneBasedMonthDeserializer extends DelegatingDeserializer {
+ public OneBasedMonthDeserializer(ValueDeserializer> defaultDeserializer) {
+ super(defaultDeserializer);
+ }
+
+ @Override
+ public Object deserialize(JsonParser parser, DeserializationContext context) {
+ JsonToken token = parser.currentToken();
+ switch (token) {
+ case VALUE_NUMBER_INT:
+ return _decodeMonth(parser.getIntValue(), parser);
+ case VALUE_STRING:
+ String monthSpec = parser.getString();
+ int oneBasedMonthNumber = _decodeNumber(monthSpec);
+ if (oneBasedMonthNumber >= 0) {
+ return _decodeMonth(oneBasedMonthNumber, parser);
+ }
+ default:
+ // Otherwise fall through to default handling
+ break;
+ }
+ return getDelegatee().deserialize(parser, context);
+ }
+
+ /**
+ * @return Numeric value of input text that represents a 1-digit or 2-digit number.
+ * Negative value in other cases (empty string, not a number, 3 or more digits).
+ */
+ private int _decodeNumber(String text) {
+ int numValue;
+ switch (text.length()) {
+ case 1:
+ char c = text.charAt(0);
+ boolean cValid = ('0' <= c && c <= '9');
+ numValue = cValid ? (c - '0') : -1;
+ break;
+ case 2:
+ char c1 = text.charAt(0);
+ char c2 = text.charAt(1);
+ boolean c12valid = ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9');
+ numValue = c12valid ? (10 * (c1 - '0') + (c2 - '0')) : -1;
+ break;
+ default:
+ numValue = -1;
+ }
+ return numValue;
+ }
+
+ private Month _decodeMonth(int oneBasedMonthNumber, JsonParser parser) throws InvalidFormatException {
+ if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) {
+ return Month.of(oneBasedMonthNumber);
+ }
+ throw new InvalidFormatException(parser, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class);
+ }
+
+ @Override
+ protected ValueDeserializer> newDelegatingInstance(ValueDeserializer> newDelegatee) {
+ return new OneBasedMonthDeserializer(newDelegatee);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java
new file mode 100644
index 0000000000..041a64866e
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearDeserializer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.StreamReadCapability;
+import tools.jackson.core.io.NumberInput;
+import tools.jackson.databind.DeserializationContext;
+
+/**
+ * Deserializer for Java 8 temporal {@link Year}s.
+ *
+ * @author Nick Williams
+ */
+public class YearDeserializer extends JSR310DateTimeDeserializerBase
+{
+ public static final YearDeserializer INSTANCE = new YearDeserializer();
+
+ /**
+ * NOTE: only {@code public} so that use via annotations (see [modules-java8#202])
+ * is possible
+ */
+ public YearDeserializer() { // public since 2.12
+ this(null);
+ }
+
+ public YearDeserializer(DateTimeFormatter formatter) {
+ super(Year.class, formatter);
+ }
+
+ /**
+ * Since 2.12
+ */
+ protected YearDeserializer(YearDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ }
+
+ /**
+ * Since 2.16
+ */
+ public YearDeserializer(YearDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape) {
+ super(base, leniency, formatter, shape);
+ }
+
+ @Override
+ protected YearDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new YearDeserializer(this, _isLenient, dtf, _shape);
+ }
+
+ @Override
+ protected YearDeserializer withLeniency(Boolean leniency) {
+ return new YearDeserializer(this, leniency);
+ }
+
+ @Override
+ public Year deserialize(JsonParser parser, DeserializationContext context) throws JacksonException
+ {
+ JsonToken t = parser.currentToken();
+ if (t == JsonToken.VALUE_STRING) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (t == JsonToken.START_OBJECT) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (t == JsonToken.VALUE_NUMBER_INT) {
+ return _fromNumber(context, parser.getIntValue());
+ }
+ if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
+ return (Year) parser.getEmbeddedObject();
+ }
+ if (parser.hasToken(JsonToken.START_ARRAY)){
+ return _deserializeFromArray(parser, context);
+ }
+ return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING, JsonToken.VALUE_NUMBER_INT);
+ }
+
+ protected Year _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ // 30-Sep-2020: Should allow use of "Timestamp as String" for XML/CSV
+ if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS)
+ && _isValidTimestampString(string)) {
+ return _fromNumber(ctxt, NumberInput.parseInt(string));
+ }
+ try {
+ if (_formatter == null) {
+ return Year.parse(string);
+ }
+ return Year.parse(string, _formatter);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, _formatter, string);
+ }
+ }
+
+ protected Year _fromNumber(DeserializationContext ctxt, int value) {
+ return Year.of(value);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java
new file mode 100644
index 0000000000..d5ae40118c
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserializer.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.DateTimeException;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.core.JsonToken;
+
+/**
+ * Deserializer for Java 8 temporal {@link YearMonth}s.
+ *
+ * @author Nick Williams
+ * @since 2.2.0
+ */
+public class YearMonthDeserializer extends JSR310DateTimeDeserializerBase
+{
+ public static final YearMonthDeserializer INSTANCE = new YearMonthDeserializer();
+
+ /**
+ * NOTE: only {@code public} so that use via annotations (see [modules-java8#202])
+ * is possible
+ */
+ public YearMonthDeserializer() // public since 2.12
+ {
+ this(DateTimeFormatter.ofPattern("u-MM"));
+ }
+
+ public YearMonthDeserializer(DateTimeFormatter formatter)
+ {
+ super(YearMonth.class, formatter);
+ }
+
+ /**
+ * Since 2.11
+ */
+ protected YearMonthDeserializer(YearMonthDeserializer base, Boolean leniency) {
+ super(base, leniency);
+ }
+
+ /**
+ * Since 2.16
+ */
+ public YearMonthDeserializer(YearMonthDeserializer base,
+ Boolean leniency,
+ DateTimeFormatter formatter,
+ JsonFormat.Shape shape) {
+ super(base, leniency, formatter, shape);
+ }
+
+ @Override
+ protected YearMonthDeserializer withDateFormat(DateTimeFormatter dtf) {
+ return new YearMonthDeserializer(this, _isLenient, dtf, _shape);
+ }
+
+ @Override
+ protected YearMonthDeserializer withLeniency(Boolean leniency) {
+ return new YearMonthDeserializer(this, leniency);
+ }
+
+ @Override
+ public YearMonth deserialize(JsonParser parser, DeserializationContext context) throws JacksonException
+ {
+ if (parser.hasToken(JsonToken.VALUE_STRING)) {
+ return _fromString(parser, context, parser.getString());
+ }
+ // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
+ if (parser.isExpectedStartObjectToken()) {
+ return _fromString(parser, context,
+ context.extractScalarFromObject(parser, this, handledType()));
+ }
+ if (parser.isExpectedStartArrayToken()) {
+ JsonToken t = parser.nextToken();
+ if (t == JsonToken.END_ARRAY) {
+ return null;
+ }
+ if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
+ && context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
+ final YearMonth parsed = deserialize(parser, context);
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ handleMissingEndArrayForSingle(parser, context);
+ }
+ return parsed;
+ }
+ if (t != JsonToken.VALUE_NUMBER_INT) {
+ _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "years");
+ }
+ int year = parser.getIntValue();
+ int month = parser.nextIntValue(-1);
+ if (month == -1) {
+ if (!parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
+ _reportWrongToken(context, JsonToken.VALUE_NUMBER_INT, "months");
+ }
+ month = parser.getIntValue();
+ }
+ if (parser.nextToken() != JsonToken.END_ARRAY) {
+ throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
+ "Expected array to end");
+ }
+ return YearMonth.of(year, month);
+ }
+ if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
+ return (YearMonth) parser.getEmbeddedObject();
+ }
+ return _handleUnexpectedToken(context, parser,
+ JsonToken.VALUE_STRING, JsonToken.START_ARRAY);
+ }
+
+ protected YearMonth _fromString(JsonParser p, DeserializationContext ctxt,
+ String string0)
+ throws JacksonException
+ {
+ String string = string0.trim();
+ if (string.length() == 0) {
+ // 22-Oct-2020, tatu: not sure if we should pass original (to distinguish
+ // b/w empty and blank); for now don't which will allow blanks to be
+ // handled like "regular" empty (same as pre-2.12)
+ return _fromEmptyString(p, ctxt, string);
+ }
+ try {
+ return YearMonth.parse(string, _formatter);
+ } catch (DateTimeException e) {
+ return _handleDateTimeFormatException(ctxt, e, _formatter, string);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java
new file mode 100644
index 0000000000..75434b3b95
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/DurationKeyDeserializer.java
@@ -0,0 +1,27 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.Duration;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class DurationKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final DurationKeyDeserializer INSTANCE = new DurationKeyDeserializer();
+
+ private DurationKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected Duration deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return Duration.parse(key);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, Duration.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java
new file mode 100644
index 0000000000..995772514e
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/InstantKeyDeserializer.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class InstantKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final InstantKeyDeserializer INSTANCE = new InstantKeyDeserializer();
+
+ private InstantKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected Instant deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return DateTimeFormatter.ISO_INSTANT.parse(key, Instant::from);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, Instant.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java
new file mode 100644
index 0000000000..33144b1c82
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/Jsr310KeyDeserializer.java
@@ -0,0 +1,41 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.KeyDeserializer;
+import tools.jackson.databind.util.ClassUtil;
+
+abstract class Jsr310KeyDeserializer extends KeyDeserializer
+{
+ @Override
+ public final Object deserializeKey(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ // 17-Aug-2019, tatu: Jackson 2.x had special handling for "null" key marker, which
+ // is why we have this unnecessary dispatching, for now
+ return deserialize(key, ctxt);
+ }
+
+ protected abstract Object deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException;
+
+ @SuppressWarnings("unchecked")
+ protected T _handleDateTimeException(DeserializationContext ctxt,
+ Class> type, DateTimeException e0, String value)
+ throws JacksonException
+ {
+ try {
+ return (T) ctxt.handleWeirdKey(type, value,
+ "Failed to deserialize %s: (%s) %s",
+ ClassUtil.nameOf(type),
+ e0.getClass().getName(),
+ e0.getMessage());
+
+ } catch (JacksonException e) {
+ e.initCause(e0);
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java
new file mode 100644
index 0000000000..944ac72f8f
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateKeyDeserializer.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class LocalDateKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final LocalDateKeyDeserializer INSTANCE = new LocalDateKeyDeserializer();
+
+ private LocalDateKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected LocalDate deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return LocalDate.parse(key, DateTimeFormatter.ISO_LOCAL_DATE);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, LocalDate.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java
new file mode 100644
index 0000000000..0e4dacaad6
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalDateTimeKeyDeserializer.java
@@ -0,0 +1,29 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class LocalDateTimeKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final LocalDateTimeKeyDeserializer INSTANCE = new LocalDateTimeKeyDeserializer();
+
+ private LocalDateTimeKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected LocalDateTime deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return LocalDateTime.parse(key, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, LocalDateTime.class, e, key);
+ }
+ }
+
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java
new file mode 100644
index 0000000000..7e327e01bc
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/LocalTimeKeyDeserializer.java
@@ -0,0 +1,29 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class LocalTimeKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final LocalTimeKeyDeserializer INSTANCE = new LocalTimeKeyDeserializer();
+
+ private LocalTimeKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected LocalTime deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return LocalTime.parse(key, DateTimeFormatter.ISO_LOCAL_TIME);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, LocalTime.class, e, key);
+ }
+ }
+
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java
new file mode 100644
index 0000000000..c8ff90150f
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/MonthDayKeyDeserializer.java
@@ -0,0 +1,40 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import static java.time.temporal.ChronoField.DAY_OF_MONTH;
+import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
+
+import java.time.DateTimeException;
+import java.time.MonthDay;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class MonthDayKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final MonthDayKeyDeserializer INSTANCE = new MonthDayKeyDeserializer();
+
+ // formatter copied from MonthDay
+ private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder()
+ .appendLiteral("--")
+ .appendValue(MONTH_OF_YEAR, 2)
+ .appendLiteral('-')
+ .appendValue(DAY_OF_MONTH, 2)
+ .toFormatter();
+
+ private MonthDayKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected MonthDay deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return MonthDay.parse(key, PARSER);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, MonthDay.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java
new file mode 100644
index 0000000000..398c33979c
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetDateTimeKeyDeserializer.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class OffsetDateTimeKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final OffsetDateTimeKeyDeserializer INSTANCE = new OffsetDateTimeKeyDeserializer();
+
+ private OffsetDateTimeKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected OffsetDateTime deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return OffsetDateTime.parse(key, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, OffsetDateTime.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java
new file mode 100644
index 0000000000..7f82cb916a
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/OffsetTimeKeyDeserializer.java
@@ -0,0 +1,29 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.OffsetTime;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class OffsetTimeKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final OffsetTimeKeyDeserializer INSTANCE = new OffsetTimeKeyDeserializer();
+
+ private OffsetTimeKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected OffsetTime deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return OffsetTime.parse(key, DateTimeFormatter.ISO_OFFSET_TIME);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, OffsetTime.class, e, key);
+ }
+ }
+
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java
new file mode 100644
index 0000000000..19c87fcc5d
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/PeriodKeyDeserializer.java
@@ -0,0 +1,27 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.Period;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class PeriodKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final PeriodKeyDeserializer INSTANCE = new PeriodKeyDeserializer();
+
+ private PeriodKeyDeserializer() {
+ // singletin
+ }
+
+ @Override
+ protected Period deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return Period.parse(key);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, Period.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java
new file mode 100644
index 0000000000..1aea9c5693
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearKeyDeserializer.java
@@ -0,0 +1,29 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.Year;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class YearKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final YearKeyDeserializer INSTANCE = new YearKeyDeserializer();
+
+ private YearKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected Year deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return Year.of(Integer.parseInt(key));
+ } catch (NumberFormatException nfe) {
+ return _handleDateTimeException(ctxt, Year.class, new DateTimeException("Number format exception", nfe), key);
+ } catch (DateTimeException dte) {
+ return _handleDateTimeException(ctxt, Year.class, dte, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java
new file mode 100644
index 0000000000..bde36b2393
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/YearMonthKeyDeserializer.java
@@ -0,0 +1,37 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
+import static java.time.temporal.ChronoField.YEAR;
+
+import java.time.DateTimeException;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.SignStyle;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class YearMonthKeyDeserializer extends Jsr310KeyDeserializer {
+ public static final YearMonthKeyDeserializer INSTANCE = new YearMonthKeyDeserializer();
+
+ // parser copied from YearMonth
+ private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
+ .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
+ .appendLiteral('-')
+ .appendValue(MONTH_OF_YEAR, 2)
+ .toFormatter();
+
+ private YearMonthKeyDeserializer() { } // singleton
+
+ @Override
+ protected YearMonth deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return YearMonth.parse(key, FORMATTER);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, YearMonth.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java
new file mode 100644
index 0000000000..9519672794
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneIdKeyDeserializer.java
@@ -0,0 +1,27 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.ZoneId;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class ZoneIdKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final ZoneIdKeyDeserializer INSTANCE = new ZoneIdKeyDeserializer();
+
+ private ZoneIdKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected Object deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return ZoneId.of(key);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, ZoneId.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java
new file mode 100644
index 0000000000..a8812bee1a
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZoneOffsetKeyDeserializer.java
@@ -0,0 +1,27 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.ZoneOffset;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class ZoneOffsetKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final ZoneOffsetKeyDeserializer INSTANCE = new ZoneOffsetKeyDeserializer();
+
+ private ZoneOffsetKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected ZoneOffset deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ return ZoneOffset.of(key);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, ZoneOffset.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java
new file mode 100644
index 0000000000..af56212b02
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializer.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.DateTimeException;
+import java.time.ZonedDateTime;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.DeserializationContext;
+
+public class ZonedDateTimeKeyDeserializer extends Jsr310KeyDeserializer {
+
+ public static final ZonedDateTimeKeyDeserializer INSTANCE = new ZonedDateTimeKeyDeserializer();
+
+ protected ZonedDateTimeKeyDeserializer() {
+ // singleton
+ }
+
+ @Override
+ protected ZonedDateTime deserialize(String key, DeserializationContext ctxt)
+ throws JacksonException
+ {
+ try {
+ // Not supplying a formatter allows the use of all supported formats
+ return ZonedDateTime.parse(key);
+ } catch (DateTimeException e) {
+ return _handleDateTimeException(ctxt, ZonedDateTime.class, e, key);
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/package-info.java b/src/main/java/tools/jackson/databind/ext/javatime/package-info.java
new file mode 100644
index 0000000000..c83bff19ea
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/package-info.java
@@ -0,0 +1,7 @@
+/**
+Contains support for Java (8) Time (JSR-310) types: always available (as of Jackson 3.0)
+but included similar to {@code JacksonModule}s for better configurability.
+
+*/
+
+package tools.jackson.databind.ext.javatime;
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java
new file mode 100644
index 0000000000..00fb9db912
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/DurationSerializer.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+import tools.jackson.databind.ext.javatime.util.DurationUnitConverter;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat;
+
+/**
+ * Serializer for Java 8 temporal {@link Duration}s.
+ *
+ * NOTE: since 2.10, {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS}
+ * determines global default used for determining if serialization should use
+ * numeric (timestamps) or textual representation. Before this,
+ * {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} was used.
+ *
+ * @author Nick Williams
+ */
+public class DurationSerializer extends JSR310FormattedSerializerBase
+{
+ public static final DurationSerializer INSTANCE = new DurationSerializer();
+
+ /**
+ * When defined (not {@code null}) duration values will be converted into integers
+ * with the unit configured for the converter.
+ * Only available when {@link SerializationFeature#WRITE_DURATIONS_AS_TIMESTAMPS} is enabled
+ * and {@link SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS} is not enabled
+ * since the duration converters do not support fractions
+ * @since 2.12
+ */
+ private DurationUnitConverter _durationUnitConverter;
+
+ protected DurationSerializer() { // was private before 2.12
+ super(Duration.class);
+ }
+
+ protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp) {
+ super(base, dtf, useTimestamp, null, null);
+ }
+
+ protected DurationSerializer(DurationSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds) {
+ super(base, dtf, useTimestamp, useNanoseconds, null);
+ }
+
+ protected DurationSerializer(DurationSerializer base, DurationUnitConverter converter) {
+ super(base, base._formatter, base._useTimestamp, base._useNanoseconds, base._shape);
+ _durationUnitConverter = converter;
+ }
+
+ @Override
+ protected DurationSerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new DurationSerializer(this, dtf, useTimestamp);
+ }
+
+ protected DurationSerializer withConverter(DurationUnitConverter converter) {
+ return new DurationSerializer(this, converter);
+ }
+
+ // @since 2.10
+ @Override
+ protected SerializationFeature getTimestampsFeature() {
+ return SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS;
+ }
+
+ @Override
+ public ValueSerializer> createContextual(SerializationContext ctxt, BeanProperty property)
+ {
+ DurationSerializer ser = (DurationSerializer) super.createContextual(ctxt, property);
+ JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
+ if (format != null && format.hasPattern()) {
+ final String pattern = format.getPattern();
+ DurationUnitConverter p = DurationUnitConverter.from(pattern);
+ if (p == null) {
+ ctxt.reportBadDefinition(handledType(),
+ String.format(
+ "Bad 'pattern' definition (\"%s\") for `Duration`: expected one of [%s]",
+ pattern, DurationUnitConverter.descForAllowed()));
+ }
+ ser = ser.withConverter(p);
+ }
+ return ser;
+ }
+
+ @Override
+ public void serialize(Duration duration, JsonGenerator generator, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ // 03-Aug-2022, tatu: As per [modules-java8#224] need to consider
+ // Pattern first, and only then nano-seconds/millis difference
+ if (_durationUnitConverter != null) {
+ generator.writeNumber(_durationUnitConverter.convert(duration));
+ } else if (useNanoseconds(ctxt)) {
+ generator.writeNumber(_toNanos(duration));
+ } else {
+ generator.writeNumber(duration.toMillis());
+ }
+ } else {
+ generator.writeString(duration.toString());
+ }
+ }
+
+ // 20-Oct-2020, tatu: [modules-java8#165] Need to take care of
+ // negative values too, and without work-around values
+ // returned are wonky wrt conversions
+ private BigDecimal _toNanos(Duration duration) {
+ BigDecimal bd;
+ if (duration.isNegative()) {
+ duration = duration.abs();
+ bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
+ duration.getNano())
+ .negate();
+ } else {
+ bd = DecimalUtils.toBigDecimal(duration.getSeconds(),
+ duration.getNano());
+ }
+ return bd;
+ }
+
+ @Override
+ protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
+ if (v2 != null) {
+ v2.numberType(JsonParser.NumberType.LONG);
+ SerializationContext ctxt = visitor.getContext();
+ if ((ctxt != null) && useNanoseconds(ctxt)) {
+ // big number, no more specific qualifier to use...
+ } else { // otherwise good old Unix timestamp, in milliseconds
+ v2.format(JsonValueFormat.UTC_MILLISEC);
+ }
+ }
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ if (useTimestamp(ctxt)) {
+ if (useNanoseconds(ctxt)) {
+ return JsonToken.VALUE_NUMBER_FLOAT;
+ }
+ return JsonToken.VALUE_NUMBER_INT;
+ }
+ return JsonToken.VALUE_STRING;
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
+ return new DurationSerializer(this, _formatter, _useTimestamp, writeNanoseconds);
+ }
+
+ @Override
+ protected DateTimeFormatter _useDateTimeFormatter(SerializationContext ctxt, JsonFormat.Value format) {
+ return null;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java
new file mode 100644
index 0000000000..5b8b401b08
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Serializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s.
+ *
+ * @author Nick Williams
+ */
+public class InstantSerializer extends InstantSerializerBase
+{
+ public static final InstantSerializer INSTANCE = new InstantSerializer();
+
+ protected InstantSerializer() {
+ super(Instant.class, Instant::toEpochMilli, Instant::getEpochSecond, Instant::getNano,
+ // null -> use 'value.toString()', default format
+ null);
+ }
+
+ /*
+ protected InstantSerializer(InstantSerializer base,
+ Boolean useTimestamp, DateTimeFormatter formatter) {
+ this(base, formatter, useTimestamp, base._useNanoseconds, base);
+ }
+ */
+
+ protected InstantSerializer(InstantSerializer base, DateTimeFormatter formatter,
+ Boolean useTimestamp, Boolean useNanoseconds,
+ JsonFormat.Shape shape) {
+ super(base, formatter, useTimestamp, useNanoseconds, shape);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter formatter,
+ Boolean useTimestamp,
+ JsonFormat.Shape shape) {
+ return new InstantSerializer(this, formatter, useTimestamp, this._useNanoseconds , shape);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
+ return new InstantSerializer(this, _formatter, _useTimestamp, writeNanoseconds,
+ this._shape);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java
new file mode 100644
index 0000000000..57f9e381cc
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializerBase.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.JsonParser.NumberType;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat;
+
+/**
+ * Base class for serializers used for {@link java.time.Instant} and
+ * other {@link Temporal} subtypes.
+ */
+public abstract class InstantSerializerBase
+ extends JSR310FormattedSerializerBase
+{
+ private final DateTimeFormatter defaultFormat;
+
+ private final ToLongFunction getEpochMillis;
+
+ private final ToLongFunction getEpochSeconds;
+
+ private final ToIntFunction getNanoseconds;
+
+ protected InstantSerializerBase(Class supportedType, ToLongFunction getEpochMillis,
+ ToLongFunction getEpochSeconds, ToIntFunction getNanoseconds,
+ DateTimeFormatter defaultFormat)
+ {
+ // Bit complicated, just because we actually want to "hide" default formatter,
+ // so that it won't accidentally force use of textual presentation
+ super(supportedType, null);
+ this.defaultFormat = defaultFormat;
+ this.getEpochMillis = getEpochMillis;
+ this.getEpochSeconds = getEpochSeconds;
+ this.getNanoseconds = getNanoseconds;
+ }
+
+ protected InstantSerializerBase(InstantSerializerBase base,
+ DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds,
+ JsonFormat.Shape shape)
+ {
+ super(base, dtf, useTimestamp, useNanoseconds, shape);
+ defaultFormat = base.defaultFormat;
+ getEpochMillis = base.getEpochMillis;
+ getEpochSeconds = base.getEpochSeconds;
+ getNanoseconds = base.getNanoseconds;
+ }
+
+ @Override
+ protected abstract JSR310FormattedSerializerBase> withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp,
+ JsonFormat.Shape shape);
+
+ @Override
+ public void serialize(T value, JsonGenerator generator, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ if (useNanoseconds(ctxt)) {
+ generator.writeNumber(DecimalUtils.toBigDecimal(
+ getEpochSeconds.applyAsLong(value), getNanoseconds.applyAsInt(value)
+ ));
+ return;
+ }
+ generator.writeNumber(getEpochMillis.applyAsLong(value));
+ return;
+ }
+
+ generator.writeString(formatValue(value, ctxt));
+ }
+
+ // Overridden to ensure that our timestamp handling is as expected
+ @Override
+ protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ if (useNanoseconds(visitor.getContext())) {
+ JsonNumberFormatVisitor v2 = visitor.expectNumberFormat(typeHint);
+ if (v2 != null) {
+ v2.numberType(NumberType.BIG_DECIMAL);
+ }
+ } else {
+ JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
+ if (v2 != null) {
+ v2.numberType(NumberType.LONG);
+ v2.format(JsonValueFormat.UTC_MILLISEC);
+ }
+ }
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ if (useTimestamp(ctxt)) {
+ if (useNanoseconds(ctxt)) {
+ return JsonToken.VALUE_NUMBER_FLOAT;
+ }
+ return JsonToken.VALUE_NUMBER_INT;
+ }
+ return JsonToken.VALUE_STRING;
+ }
+
+ protected String formatValue(T value, SerializationContext ctxt)
+ {
+ DateTimeFormatter formatter = (_formatter == null) ? defaultFormat :_formatter;
+ if (formatter != null) {
+ if (formatter.getZone() == null) { // timezone set if annotated on property
+ // If the user specified to use the context TimeZone explicitly, and the formatter provided doesn't contain a TZ
+ // Then we use the TZ specified in the objectMapper
+ if (ctxt.getConfig().hasExplicitTimeZone()
+ && ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE)) {
+ formatter = formatter.withZone(ctxt.getTimeZone().toZoneId());
+ }
+ }
+ return formatter.format(value);
+ }
+
+ return value.toString();
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java
new file mode 100644
index 0000000000..32c2a6311a
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310FormattedSerializerBase.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Locale;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Shape;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.jsonFormatVisitors.*;
+
+/**
+ * Base class that provides an array schema instead of scalar schema if
+ * {@link SerializationFeature#WRITE_DATES_AS_TIMESTAMPS} is enabled.
+ *
+ * @author Nick Williams
+ */
+abstract class JSR310FormattedSerializerBase
+ extends JSR310SerializerBase
+{
+ /**
+ * Flag that indicates that serialization must be done as the
+ * Java timestamp, regardless of other settings.
+ */
+ protected final Boolean _useTimestamp;
+
+ /**
+ * Flag that indicates that numeric timestamp values must be written using
+ * nanosecond timestamps if the datatype supports such resolution,
+ * regardless of other settings.
+ */
+ protected final Boolean _useNanoseconds;
+
+ /**
+ * Specific format to use, if not default format: non-null value
+ * also indicates that serialization is to be done as JSON String,
+ * not numeric timestamp, unless {@code #_useTimestamp} is true.
+ */
+ protected final DateTimeFormatter _formatter;
+
+ protected final JsonFormat.Shape _shape;
+
+ /**
+ * Lazily constructed {@code JavaType} representing type
+ * {@code List}.
+ *
+ * @since 2.10
+ */
+ protected transient volatile JavaType _integerListType;
+
+ protected JSR310FormattedSerializerBase(Class supportedType) {
+ this(supportedType, null);
+ }
+
+ protected JSR310FormattedSerializerBase(Class supportedType,
+ DateTimeFormatter formatter) {
+ super(supportedType);
+ _useTimestamp = null;
+ _useNanoseconds = null;
+ _shape = null;
+ _formatter = formatter;
+ }
+
+ /*
+
+ protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase> base,
+ DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape)
+ {
+ this(base, dtf, useTimestamp, null, shape);
+ }
+ */
+
+ protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase> base,
+ DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds,
+ JsonFormat.Shape shape)
+ {
+ super(base.handledType());
+ _useTimestamp = useTimestamp;
+ _useNanoseconds = useNanoseconds;
+ _formatter = dtf;
+ _shape = shape;
+ }
+
+ protected abstract JSR310FormattedSerializerBase> withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape);
+
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId,
+ Boolean writeNanoseconds) {
+ return this;
+ }
+
+ @Override
+ public ValueSerializer> createContextual(SerializationContext ctxt,
+ BeanProperty property)
+ {
+ JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
+ if (format != null) {
+ Boolean useTimestamp = null;
+
+ // Simple case first: serialize as numeric timestamp?
+ JsonFormat.Shape shape = format.getShape();
+ if (shape == JsonFormat.Shape.ARRAY || shape.isNumeric() ) {
+ useTimestamp = Boolean.TRUE;
+ } else {
+ useTimestamp = (shape == JsonFormat.Shape.STRING) ? Boolean.FALSE : null;
+ }
+ DateTimeFormatter dtf = _formatter;
+
+ // If not, do we have a pattern?
+ if (format.hasPattern()) {
+ dtf = _useDateTimeFormatter(ctxt, format);
+ }
+ JSR310FormattedSerializerBase> ser = this;
+ if ((shape != _shape) || (useTimestamp != _useTimestamp) || (dtf != _formatter)) {
+ ser = ser.withFormat(dtf, useTimestamp, shape);
+ }
+ Boolean writeZoneId = format.getFeature(JsonFormat.Feature.WRITE_DATES_WITH_ZONE_ID);
+ Boolean writeNanoseconds = format.getFeature(JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ if ((writeZoneId != null) || (writeNanoseconds != null)) {
+ ser = ser.withFeatures(writeZoneId, writeNanoseconds);
+ }
+ return ser;
+ }
+ return this;
+ }
+
+ /**
+ * @deprecated Since 2.15
+ */
+ @Deprecated
+ @Override
+ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ if (useTimestamp(visitor.getContext())) {
+ _acceptTimestampVisitor(visitor, typeHint);
+ } else {
+ JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint);
+ if (v2 != null) {
+ v2.format(JsonValueFormat.DATE_TIME);
+ }
+ }
+ }
+
+ protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ // By default, most sub-types use JSON Array, so do this:
+ // 28-May-2019, tatu: serialized as a List, presumably
+ JsonArrayFormatVisitor v2 = visitor.expectArrayFormat(_integerListType(visitor.getContext()));
+ if (v2 != null) {
+ v2.itemsFormat(JsonFormatTypes.INTEGER);
+ }
+ }
+
+ protected JavaType _integerListType(SerializationContext ctxt) {
+ JavaType t = _integerListType;
+ if (t == null) {
+ t = ctxt.getTypeFactory()
+ .constructCollectionType(List.class, Integer.class);
+ _integerListType = t;
+ }
+ return t;
+ }
+
+ /**
+ * Overridable method that determines {@link SerializationFeature} that is used as
+ * the global default in determining if date/time value serialized should use numeric
+ * format ("timestamp") or not.
+ *
+ * Note that this feature is just the baseline setting and may be overridden on per-type
+ * or per-property basis.
+ *
+ * @since 2.10
+ */
+ protected SerializationFeature getTimestampsFeature() {
+ return SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
+ }
+
+ protected boolean useTimestamp(SerializationContext ctxt) {
+ if (_useTimestamp != null) {
+ return _useTimestamp.booleanValue();
+ }
+ if (_shape != null) {
+ if (_shape == Shape.STRING) {
+ return false;
+ }
+ if (_shape == Shape.NUMBER_INT) {
+ return true;
+ }
+ }
+ // assume that explicit formatter definition implies use of textual format
+ return (_formatter == null) && useTimestampFromGlobalDefaults(ctxt);
+ }
+
+ protected boolean useTimestampFromGlobalDefaults(SerializationContext ctxt) {
+ return (ctxt != null)
+ && ctxt.isEnabled(getTimestampsFeature());
+ }
+
+ protected boolean _useTimestampExplicitOnly(SerializationContext ctxt) {
+ if (_useTimestamp != null) {
+ return _useTimestamp.booleanValue();
+ }
+ return false;
+ }
+
+ protected boolean useNanoseconds(SerializationContext ctxt) {
+ if (_useNanoseconds != null) {
+ return _useNanoseconds.booleanValue();
+ }
+ if (_shape != null) {
+ if (_shape == Shape.NUMBER_INT) {
+ return false;
+ }
+ if (_shape == Shape.NUMBER_FLOAT) {
+ return true;
+ }
+ }
+ return (ctxt != null)
+ && ctxt.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ // modules-java8#189: to be overridden by other formatters using this as base class
+ protected DateTimeFormatter _useDateTimeFormatter(SerializationContext ctxt, JsonFormat.Value format) {
+ DateTimeFormatter dtf;
+ final String pattern = format.getPattern();
+ final Locale locale = format.hasLocale() ? format.getLocale() : ctxt.getLocale();
+ if (locale == null) {
+ dtf = DateTimeFormatter.ofPattern(pattern);
+ } else {
+ dtf = DateTimeFormatter.ofPattern(pattern, locale);
+ }
+ //Issue #69: For instant serializers/deserializers we need to configure the formatter with
+ //a time zone picked up from JsonFormat annotation, otherwise serialization might not work
+ if (format.hasTimeZone()) {
+ dtf = dtf.withZone(format.getTimeZone().toZoneId());
+ }
+ return dtf;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java
new file mode 100644
index 0000000000..0c208768b7
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JSR310SerializerBase.java
@@ -0,0 +1,40 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsontype.TypeSerializer;
+import tools.jackson.databind.ser.std.StdSerializer;
+
+/**
+ * Base class that indicates that all JSR310 datatypes are serialized as scalar JSON types.
+ *
+ * @author Nick Williams
+ */
+abstract class JSR310SerializerBase extends StdSerializer
+{
+ protected JSR310SerializerBase(Class> supportedType) {
+ super(supportedType);
+ }
+
+ @Override
+ public void serializeWithType(T value, JsonGenerator g, SerializationContext ctxt,
+ TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ serialize(value, g, ctxt);
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ /**
+ * Overridable helper method used from {@link #serializeWithType}, to indicate
+ * shape of value during serialization; needed to know how type id is to be
+ * serialized.
+ */
+ protected abstract JsonToken serializationShape(SerializationContext ctxt);
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java
new file mode 100644
index 0000000000..33c0b12ffe
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/JavaTimeSerializerModifier.java
@@ -0,0 +1,28 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Month;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ser.ValueSerializerModifier;
+
+/**
+ * @since 2.17
+ */
+public class JavaTimeSerializerModifier extends ValueSerializerModifier {
+ private static final long serialVersionUID = 1L;
+
+ private final boolean _oneBaseMonths;
+
+ public JavaTimeSerializerModifier(boolean oneBaseMonths) {
+ _oneBaseMonths = oneBaseMonths;
+ }
+
+ @Override
+ public ValueSerializer> modifyEnumSerializer(SerializationConfig config, JavaType valueType,
+ BeanDescription beanDesc, ValueSerializer> serializer) {
+ if (_oneBaseMonths && valueType.hasRawClass(Month.class)) {
+ return new OneBasedMonthSerializer(serializer);
+ }
+ return serializer;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java
new file mode 100644
index 0000000000..ba1d4b2879
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerializer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+import tools.jackson.databind.*;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat;
+import tools.jackson.databind.jsontype.TypeSerializer;
+
+/**
+ * Serializer for Java 8 temporal {@link LocalDate}s.
+ *
+ * @author Nick Williams
+ */
+public class LocalDateSerializer extends JSR310FormattedSerializerBase
+{
+ public static final LocalDateSerializer INSTANCE = new LocalDateSerializer();
+
+ protected LocalDateSerializer() {
+ super(LocalDate.class);
+ }
+
+ protected LocalDateSerializer(LocalDateSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ super(base, dtf, useTimestamp, null, shape);
+ }
+
+ public LocalDateSerializer(DateTimeFormatter formatter) {
+ super(LocalDate.class, formatter);
+ }
+
+ @Override
+ protected LocalDateSerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new LocalDateSerializer(this, dtf, useTimestamp, shape);
+ }
+
+ @Override
+ public void serialize(LocalDate date, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ if (_shape == JsonFormat.Shape.NUMBER_INT) {
+ g.writeNumber(date.toEpochDay());
+ } else {
+ g.writeStartArray();
+ _serializeAsArrayContents(date, g, ctxt);
+ g.writeEndArray();
+ }
+ } else {
+ g.writeString((_formatter == null) ? date.toString() : date.format(_formatter));
+ }
+ }
+
+ @Override
+ public void serializeWithType(LocalDate value, JsonGenerator g,
+ SerializationContext ctxt, TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ JsonToken shape = (typeIdDef == null) ? null : typeIdDef.valueShape;
+ if (shape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else if (shape == JsonToken.VALUE_NUMBER_INT) {
+ g.writeNumber(value.toEpochDay());
+ } else {
+ g.writeString((_formatter == null) ? value.toString() : value.format(_formatter));
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ protected void _serializeAsArrayContents(LocalDate value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getYear());
+ g.writeNumber(value.getMonthValue());
+ g.writeNumber(value.getDayOfMonth());
+ }
+
+ @Override
+ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ SerializationContext ctxt = visitor.getContext();
+ boolean useTimestamp = (ctxt != null) && useTimestamp(ctxt);
+ if (useTimestamp) {
+ _acceptTimestampVisitor(visitor, typeHint);
+ } else {
+ JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint);
+ if (v2 != null) {
+ v2.format(JsonValueFormat.DATE);
+ }
+ }
+ }
+
+ @Override // since 2.9
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ if (useTimestamp(ctxt)) {
+ if (_shape == JsonFormat.Shape.NUMBER_INT) {
+ return JsonToken.VALUE_NUMBER_INT;
+ }
+ return JsonToken.START_ARRAY;
+ }
+ return JsonToken.VALUE_STRING;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java
new file mode 100644
index 0000000000..540202a2ec
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsontype.TypeSerializer;
+
+/**
+ * Serializer for Java 8 temporal {@link LocalDateTime}s.
+ *
+ * @author Nick Williams
+ */
+public class LocalDateTimeSerializer extends JSR310FormattedSerializerBase
+{
+ public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
+
+ protected LocalDateTimeSerializer() {
+ this(null);
+ }
+
+ public LocalDateTimeSerializer(DateTimeFormatter f) {
+ super(LocalDateTime.class, f);
+ }
+
+ protected LocalDateTimeSerializer(LocalDateTimeSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds) {
+ super(base, dtf, useTimestamp, useNanoseconds, null);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter f,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new LocalDateTimeSerializer(this, f, useTimestamp, _useNanoseconds);
+ }
+
+ protected DateTimeFormatter _defaultFormatter() {
+ return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+ }
+
+ @Override
+ public void serialize(LocalDateTime value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ g.writeStartArray();
+ _serializeAsArrayContents(value, g, ctxt);
+ g.writeEndArray();
+ } else {
+ DateTimeFormatter dtf = _formatter;
+ if (dtf == null) {
+ dtf = _defaultFormatter();
+ }
+ g.writeString(value.format(dtf));
+ }
+ }
+
+ @Override
+ public void serializeWithType(LocalDateTime value, JsonGenerator g, SerializationContext ctxt,
+ TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ if ((typeIdDef != null)
+ && typeIdDef.valueShape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else {
+ DateTimeFormatter dtf = _formatter;
+ if (dtf == null) {
+ dtf = _defaultFormatter();
+ }
+ g.writeString(value.format(dtf));
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ private final void _serializeAsArrayContents(LocalDateTime value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getYear());
+ g.writeNumber(value.getMonthValue());
+ g.writeNumber(value.getDayOfMonth());
+ g.writeNumber(value.getHour());
+ g.writeNumber(value.getMinute());
+ final int secs = value.getSecond();
+ final int nanos = value.getNano();
+ if ((secs > 0) || (nanos > 0)) {
+ g.writeNumber(secs);
+ if (nanos > 0) {
+ if (useNanoseconds(ctxt)) {
+ g.writeNumber(nanos);
+ } else {
+ g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING;
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
+ return new LocalDateTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java
new file mode 100644
index 0000000000..8ae9a92428
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerializer.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat;
+import tools.jackson.databind.jsontype.TypeSerializer;
+/**
+ * Serializer for Java 8 temporal {@link LocalTime}s.
+ *
+ * @author Nick Williams
+ * @since 2.2
+ */
+public class LocalTimeSerializer extends JSR310FormattedSerializerBase
+{
+ public static final LocalTimeSerializer INSTANCE = new LocalTimeSerializer();
+
+ protected LocalTimeSerializer() {
+ this(null);
+ }
+
+ public LocalTimeSerializer(DateTimeFormatter formatter) {
+ super(LocalTime.class, formatter);
+ }
+
+ protected LocalTimeSerializer(LocalTimeSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds) {
+ super(base, dtf, useTimestamp, useNanoseconds, null);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new LocalTimeSerializer(this, dtf, useTimestamp, _useNanoseconds);
+ }
+
+ // since 2.7: TODO in 3.x; change to use per-type defaulting
+ protected DateTimeFormatter _defaultFormatter() {
+ return DateTimeFormatter.ISO_LOCAL_TIME;
+ }
+
+ @Override
+ public void serialize(LocalTime value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ g.writeStartArray();
+ _serializeAsArrayContents(value, g, ctxt);
+ g.writeEndArray();
+ } else {
+ DateTimeFormatter dtf = _formatter;
+ if (dtf == null) {
+ dtf = _defaultFormatter();
+ }
+ g.writeString(value.format(dtf));
+ }
+ }
+
+ @Override
+ public void serializeWithType(LocalTime value, JsonGenerator g,
+ SerializationContext ctxt, TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ if ((typeIdDef != null)
+ && typeIdDef.valueShape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else {
+ DateTimeFormatter dtf = _formatter;
+ if (dtf == null) {
+ dtf = _defaultFormatter();
+ }
+ g.writeString(value.format(dtf));
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ private final void _serializeAsArrayContents(LocalTime value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getHour());
+ g.writeNumber(value.getMinute());
+ int secs = value.getSecond();
+ int nanos = value.getNano();
+ if ((secs > 0) || (nanos > 0))
+ {
+ g.writeNumber(secs);
+ if (nanos > 0) {
+ if (useNanoseconds(ctxt)) {
+ g.writeNumber(nanos);
+ } else {
+ g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING;
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean useNanoseconds) {
+ return new LocalTimeSerializer(this, _formatter,
+ _useTimestamp, useNanoseconds);
+ }
+
+ // as per [modules-java8#105]
+ @Override
+ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ if (useTimestamp(visitor.getContext())) {
+ _acceptTimestampVisitor(visitor, typeHint);
+ } else {
+ JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint);
+ if (v2 != null) {
+ v2.format(JsonValueFormat.TIME);
+ }
+ }
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java
new file mode 100644
index 0000000000..e2124940ff
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerializer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.MonthDay;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsontype.TypeSerializer;
+
+/**
+ * Serializer for Java 8 temporal {@link MonthDay}s.
+ *
+ * NOTE: unlike many other date/time type serializers, this serializer will only
+ * use Array notation if explicitly instructed to do so with JsonFormat
+ * (either directly or through per-type defaults) and NOT with global defaults.
+ *
+ * @since 2.7.1
+ */
+public class MonthDaySerializer extends JSR310FormattedSerializerBase
+{
+ public static final MonthDaySerializer INSTANCE = new MonthDaySerializer();
+
+ protected MonthDaySerializer() { // was private before 2.12
+ this(null);
+ }
+
+ public MonthDaySerializer(DateTimeFormatter formatter) {
+ super(MonthDay.class, formatter);
+ }
+
+ private MonthDaySerializer(MonthDaySerializer base, DateTimeFormatter dtf, Boolean useTimestamp) {
+ super(base, dtf, useTimestamp, null, null);
+ }
+
+ @Override
+ protected MonthDaySerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new MonthDaySerializer(this, dtf, useTimestamp);
+ }
+
+ @Override
+ public void serialize(MonthDay value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (_useTimestampExplicitOnly(ctxt)) {
+ g.writeStartArray();
+ _serializeAsArrayContents(value, g, ctxt);
+ g.writeEndArray();
+ } else {
+ g.writeString((_formatter == null) ? value.toString() : value.format(_formatter));
+ }
+ }
+
+ @Override
+ public void serializeWithType(MonthDay value, JsonGenerator g,
+ SerializationContext ctxt, TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ if ((typeIdDef != null)
+ && typeIdDef.valueShape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else {
+ g.writeString((_formatter == null) ? value.toString() : value.format(_formatter));
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ protected void _serializeAsArrayContents(MonthDay value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getMonthValue());
+ g.writeNumber(value.getDayOfMonth());
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return _useTimestampExplicitOnly(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java
new file mode 100644
index 0000000000..968e8f9ec7
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java
@@ -0,0 +1,43 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class OffsetDateTimeSerializer extends InstantSerializerBase
+{
+ public static final OffsetDateTimeSerializer INSTANCE = new OffsetDateTimeSerializer();
+
+ protected OffsetDateTimeSerializer() {
+ super(OffsetDateTime.class, dt -> dt.toInstant().toEpochMilli(),
+ OffsetDateTime::toEpochSecond, OffsetDateTime::getNano,
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
+ protected OffsetDateTimeSerializer(OffsetDateTimeSerializer base,
+ Boolean useTimestamp, DateTimeFormatter formatter,
+ JsonFormat.Shape shape) {
+ this(base, formatter, useTimestamp, base._useNanoseconds, shape);
+ }
+
+ protected OffsetDateTimeSerializer(OffsetDateTimeSerializer base,
+ DateTimeFormatter formatter,
+ Boolean useTimestamp, Boolean useNanoseconds,
+ JsonFormat.Shape shape) {
+ super(base, formatter, useTimestamp, useNanoseconds, shape);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFormat(DateTimeFormatter formatter,
+ Boolean useTimestamp,
+ JsonFormat.Shape shape)
+ {
+ return new OffsetDateTimeSerializer(this, useTimestamp, formatter, shape);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
+ return new OffsetDateTimeSerializer(this, _formatter,
+ _useTimestamp, writeNanoseconds, _shape);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java
new file mode 100644
index 0000000000..46dadbbdce
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerializer.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.OffsetTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsontype.TypeSerializer;
+
+/**
+ * Serializer for Java 8 temporal {@link OffsetTime}s.
+ *
+ * @author Nick Williams
+ */
+public class OffsetTimeSerializer extends JSR310FormattedSerializerBase
+{
+ public static final OffsetTimeSerializer INSTANCE = new OffsetTimeSerializer();
+
+ protected OffsetTimeSerializer() {
+ super(OffsetTime.class);
+ }
+
+ protected OffsetTimeSerializer(OffsetTimeSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp) {
+ this(base, dtf, useTimestamp, null);
+ }
+
+ protected OffsetTimeSerializer(OffsetTimeSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp, Boolean useNanoseconds) {
+ super(base, dtf, useTimestamp, useNanoseconds, null);
+ }
+
+ @Override
+ protected OffsetTimeSerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new OffsetTimeSerializer(this, dtf, useTimestamp);
+ }
+
+ @Override
+ public void serialize(OffsetTime time, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ g.writeStartArray();
+ _serializeAsArrayContents(time, g, ctxt);
+ g.writeEndArray();
+ } else {
+ String str = (_formatter == null) ? time.toString() : time.format(_formatter);
+ g.writeString(str);
+ }
+ }
+
+ @Override
+ public void serializeWithType(OffsetTime value, JsonGenerator g, SerializationContext ctxt,
+ TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ if ((typeIdDef != null)
+ && typeIdDef.valueShape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else {
+ String str = (_formatter == null) ? value.toString() : value.format(_formatter);
+ g.writeString(str);
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ private final void _serializeAsArrayContents(OffsetTime value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getHour());
+ g.writeNumber(value.getMinute());
+ final int secs = value.getSecond();
+ final int nanos = value.getNano();
+ if ((secs > 0) || (nanos > 0)) {
+ g.writeNumber(secs);
+ if (nanos > 0) {
+ if(useNanoseconds(ctxt)) {
+ g.writeNumber(nanos);
+ } else {
+ g.writeNumber(value.get(ChronoField.MILLI_OF_SECOND));
+ }
+ }
+ }
+ g.writeString(value.getOffset().toString());
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING;
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boolean writeNanoseconds) {
+ return new OffsetTimeSerializer(this, _formatter, _useTimestamp, writeNanoseconds);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java
new file mode 100644
index 0000000000..f2680cf18e
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerializer.java
@@ -0,0 +1,33 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Month;
+
+import tools.jackson.core.JsonGenerator;
+
+import tools.jackson.databind.*;
+
+/**
+ * @since 2.17
+ */
+public class OneBasedMonthSerializer extends ValueSerializer {
+ private final ValueSerializer _defaultSerializer;
+
+ @SuppressWarnings("unchecked")
+ public OneBasedMonthSerializer(ValueSerializer> defaultSerializer)
+ {
+ _defaultSerializer = (ValueSerializer) defaultSerializer;
+ }
+
+ @Override
+ public void serialize(Month value, JsonGenerator gen, SerializationContext ctxt)
+ {
+ // 15-Jan-2024, tatu: [modules-java8#274] This is not really sufficient
+ // (see `jackson-databind` `EnumSerializer` for full logic), but has to
+ // do for now. May need to add `@JsonFormat.shape` handling in future.
+ if (ctxt.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX)) {
+ gen.writeNumber(value.ordinal() + 1);
+ return;
+ }
+ _defaultSerializer.serialize(value, gen, ctxt);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java
new file mode 100644
index 0000000000..32659a1275
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearMonthSerializer.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
+import tools.jackson.databind.jsonFormatVisitors.JsonValueFormat;
+import tools.jackson.databind.jsontype.TypeSerializer;
+
+/**
+ * Serializer for Java 8 temporal {@link YearMonth}s.
+ *
+ * @author Nick Williams
+ * @since 2.2
+ */
+public class YearMonthSerializer extends JSR310FormattedSerializerBase
+{
+ public static final YearMonthSerializer INSTANCE = new YearMonthSerializer();
+
+ protected YearMonthSerializer() { // was private before 2.12
+ this(null);
+ }
+
+ public YearMonthSerializer(DateTimeFormatter formatter) {
+ super(YearMonth.class, formatter);
+ }
+
+ private YearMonthSerializer(YearMonthSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp) {
+ super(base, dtf, useTimestamp, null, null);
+ }
+
+ @Override
+ protected YearMonthSerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new YearMonthSerializer(this, dtf, useTimestamp);
+ }
+
+ @Override
+ public void serialize(YearMonth value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ g.writeStartArray();
+ _serializeAsArrayContents(value, g, ctxt);
+ g.writeEndArray();
+ return;
+ }
+ g.writeString((_formatter == null) ? value.toString() : value.format(_formatter));
+ }
+
+ @Override
+ public void serializeWithType(YearMonth value, JsonGenerator g,
+ SerializationContext ctxt, TypeSerializer typeSer)
+ throws JacksonException
+ {
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, serializationShape(ctxt)));
+ // need to write out to avoid double-writing array markers
+ if ((typeIdDef != null)
+ && typeIdDef.valueShape == JsonToken.START_ARRAY) {
+ _serializeAsArrayContents(value, g, ctxt);
+ } else {
+ g.writeString((_formatter == null) ? value.toString() : value.format(_formatter));
+ }
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ protected void _serializeAsArrayContents(YearMonth value, JsonGenerator g,
+ SerializationContext ctxt)
+ throws JacksonException
+ {
+ g.writeNumber(value.getYear());
+ g.writeNumber(value.getMonthValue());
+ }
+
+ @Override
+ protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ SerializationContext ctxt = visitor.getContext();
+ boolean useTimestamp = (ctxt != null) && useTimestamp(ctxt);
+ if (useTimestamp) {
+ super._acceptTimestampVisitor(visitor, typeHint);
+ } else {
+ JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint);
+ if (v2 != null) {
+ v2.format(JsonValueFormat.DATE_TIME);
+ }
+ }
+ }
+
+ @Override // since 2.9
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return useTimestamp(ctxt) ? JsonToken.START_ARRAY : JsonToken.VALUE_STRING;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java
new file mode 100644
index 0000000000..302591df3a
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/YearSerializer.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import tools.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
+
+/**
+ * Serializer for Java 8 temporal {@link Year}s.
+ *
+ * @author Nick Williams
+ */
+public class YearSerializer extends JSR310FormattedSerializerBase
+{
+ public static final YearSerializer INSTANCE = new YearSerializer();
+
+ protected YearSerializer() {
+ this(null);
+ }
+
+ public YearSerializer(DateTimeFormatter formatter) {
+ super(Year.class, formatter);
+ }
+
+ protected YearSerializer(YearSerializer base, DateTimeFormatter dtf,
+ Boolean useTimestamp) {
+ super(base, dtf, useTimestamp, null, null);
+ }
+
+ @Override
+ protected YearSerializer withFormat(DateTimeFormatter dtf,
+ Boolean useTimestamp, JsonFormat.Shape shape) {
+ return new YearSerializer(this, dtf, useTimestamp);
+ }
+
+ // Need to ensure Year still defaults to numeric ("timestamp") regardless
+ // of general global setting (since that defaults to textual in Jackson 3.x)
+ @Override
+ protected boolean useTimestampFromGlobalDefaults(SerializationContext ctxt) {
+ return true;
+ }
+
+ @Override
+ public void serialize(Year year, JsonGenerator generator, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (useTimestamp(ctxt)) {
+ generator.writeNumber(year.getValue());
+ } else {
+ String str = (_formatter == null) ? year.toString() : year.format(_formatter);
+ generator.writeString(str);
+ }
+ }
+
+ // Override because we have String/Int, NOT String/Array
+ @Override
+ protected void _acceptTimestampVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
+ {
+ JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
+ if (v2 != null) {
+ v2.numberType(JsonParser.NumberType.LONG);
+ }
+ }
+
+ @Override // since 2.9
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ return useTimestamp(ctxt) ? JsonToken.VALUE_NUMBER_INT : JsonToken.VALUE_STRING;
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java
new file mode 100644
index 0000000000..af75de87ca
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZoneIdSerializer.java
@@ -0,0 +1,33 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.ZoneId;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.WritableTypeId;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.jsontype.TypeSerializer;
+import tools.jackson.databind.ser.std.ToStringSerializerBase;
+
+public class ZoneIdSerializer extends ToStringSerializerBase
+{
+ public ZoneIdSerializer() { super(ZoneId.class); }
+
+ @Override
+ public void serializeWithType(Object value, JsonGenerator g,
+ SerializationContext ctxt, TypeSerializer typeSer)
+ throws JacksonException
+ {
+ // Better ensure we don't use specific sub-classes:
+ WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
+ typeSer.typeId(value, ZoneId.class, JsonToken.VALUE_STRING));
+ serialize(value, g, ctxt);
+ typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
+ }
+
+ @Override
+ public String valueToString(Object value) {
+ return value.toString();
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java
new file mode 100644
index 0000000000..8240cac337
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java
@@ -0,0 +1,107 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonToken;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.SerializationFeature;
+
+public class ZonedDateTimeSerializer extends InstantSerializerBase {
+ public static final ZonedDateTimeSerializer INSTANCE = new ZonedDateTimeSerializer();
+
+ /**
+ * Flag for JsonFormat.Feature.WRITE_DATES_WITH_ZONE_ID
+ */
+ protected final Boolean _writeZoneId;
+
+ protected ZonedDateTimeSerializer() {
+ // ISO_ZONED_DATE_TIME is an extended version of ISO compliant format
+ // ISO_OFFSET_DATE_TIME with additional information :Zone Id
+ // (This is not part of the ISO-8601 standard)
+ this(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
+ public ZonedDateTimeSerializer(DateTimeFormatter formatter) {
+ super(ZonedDateTime.class, dt -> dt.toInstant().toEpochMilli(),
+ ZonedDateTime::toEpochSecond, ZonedDateTime::getNano,
+ formatter);
+ _writeZoneId = null;
+ }
+
+ protected ZonedDateTimeSerializer(ZonedDateTimeSerializer base,
+ DateTimeFormatter formatter,
+ Boolean useTimestamp, Boolean useNanoseconds,
+ Boolean writeZoneId,
+ JsonFormat.Shape shape)
+ {
+ super(base, formatter, useTimestamp, useNanoseconds, shape);
+ _writeZoneId = writeZoneId;
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFormat(DateTimeFormatter formatter,
+ Boolean useTimestamp,
+ JsonFormat.Shape shape)
+ {
+ return new ZonedDateTimeSerializer(this, formatter,
+ useTimestamp, _useNanoseconds, _writeZoneId,
+ shape);
+ }
+
+ @Override
+ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId,
+ Boolean useNanoseconds)
+ {
+ return new ZonedDateTimeSerializer(this, _formatter,
+ _useTimestamp, useNanoseconds, writeZoneId, _shape);
+ }
+
+ @Override
+ public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ if (!useTimestamp(ctxt)) {
+ // [modules-java8#333]: `@JsonFormat` with pattern should override
+ // `SerializationFeature.WRITE_DATES_WITH_ZONE_ID`
+ if ((_formatter != null) && (_shape == JsonFormat.Shape.STRING)) {
+ ; // use default handling
+ } else if (shouldWriteWithZoneId(ctxt)) {
+ // write with zone
+ g.writeString(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value));
+ return;
+ }
+ }
+ super.serialize(value, g, ctxt);
+ }
+
+ @Override
+ protected String formatValue(ZonedDateTime value, SerializationContext ctxt) {
+ String formatted = super.formatValue(value, ctxt);
+ // [modules-java8#333]: `@JsonFormat` with pattern should override
+ // `SerializationFeature.WRITE_DATES_WITH_ZONE_ID`
+ if (_formatter != null && _shape == JsonFormat.Shape.STRING) {
+ // Why not `if (shouldWriteWithZoneId(provider))` ?
+ if (Boolean.TRUE.equals(_writeZoneId)) {
+ formatted += "[" + value.getZone().getId() + "]";
+ }
+ }
+ return formatted;
+ }
+ public boolean shouldWriteWithZoneId(SerializationContext ctxt) {
+ return (_writeZoneId != null) ? _writeZoneId :
+ ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
+ }
+
+ @Override
+ protected JsonToken serializationShape(SerializationContext ctxt) {
+ if (!useTimestamp(ctxt) && shouldWriteWithZoneId(ctxt)) {
+ return JsonToken.VALUE_STRING;
+ }
+ return super.serializationShape(ctxt);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java
new file mode 100644
index 0000000000..3c0ef465a3
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/key/ZonedDateTimeKeySerializer.java
@@ -0,0 +1,49 @@
+package tools.jackson.databind.ext.javatime.ser.key;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.SerializationFeature;
+
+public class ZonedDateTimeKeySerializer extends ValueSerializer {
+
+ public static final ZonedDateTimeKeySerializer INSTANCE = new ZonedDateTimeKeySerializer();
+
+ private ZonedDateTimeKeySerializer() {
+ // singleton
+ }
+
+ @Override
+ public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext ctxt)
+ throws JacksonException
+ {
+ /* [modules-java8#127]: Serialization of timezone data is disabled by default, but can be
+ * turned on by enabling `SerializationFeature.WRITE_DATES_WITH_ZONE_ID`
+ */
+ if (ctxt.isEnabled(SerializationFeature.WRITE_DATES_WITH_ZONE_ID)) {
+ g.writeName(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value));
+ } else if (useTimestamps(ctxt)) {
+ if (useNanos(ctxt)) {
+ g.writeName(DecimalUtils.toBigDecimal(value.toEpochSecond(), value.getNano()).toString());
+ } else {
+ g.writeName(String.valueOf(value.toInstant().toEpochMilli()));
+ }
+ } else {
+ g.writeName(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value));
+ }
+ }
+
+ private static boolean useNanos(SerializationContext ctxt) {
+ return ctxt.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ }
+
+ private static boolean useTimestamps(SerializationContext ctxt) {
+ return ctxt.isEnabled(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java b/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java
new file mode 100644
index 0000000000..67634e9476
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/util/DecimalUtils.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.util;
+
+import tools.jackson.core.io.NumberInput;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.util.function.BiFunction;
+
+/**
+ * Utilities to aid in the translation of decimal types to/from multiple parts.
+ *
+ * @author Nick Williams
+ */
+public final class DecimalUtils
+{
+ private DecimalUtils() { }
+
+ public static String toDecimal(long seconds, int nanoseconds)
+ {
+ StringBuilder sb = new StringBuilder(20)
+ .append(seconds)
+ .append('.');
+ // 14-Mar-2016, tatu: Although we do not yet (with 2.7) trim trailing zeroes,
+ // for general case,
+ if (nanoseconds == 0L) {
+ // !!! TODO: 14-Mar-2016, tatu: as per [datatype-jsr310], should trim
+ // trailing zeroes
+ if (seconds == 0L) {
+ return "0.0";
+ }
+
+// sb.append('0');
+ sb.append("000000000");
+ } else {
+ StringBuilder nanoSB = new StringBuilder(9);
+ nanoSB.append(nanoseconds);
+ // May need to both prepend leading nanos (if value less than 0.1)
+ final int nanosLen = nanoSB.length();
+ int prepZeroes = 9 - nanosLen;
+ while (prepZeroes > 0) {
+ --prepZeroes;
+ sb.append('0');
+ }
+
+ // !!! TODO: 14-Mar-2016, tatu: as per [datatype-jsr310], should trim
+ // trailing zeroes
+ /*
+ // AND possibly trim trailing ones
+ int i = nanosLen;
+ while ((i > 1) && nanoSB.charAt(i-1) == '0') {
+ --i;
+ }
+ if (i < nanosLen) {
+ nanoSB.setLength(i);
+ }
+ */
+ sb.append(nanoSB);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Factory method for constructing {@link BigDecimal} out of second, nano-second
+ * components.
+ */
+ public static BigDecimal toBigDecimal(long seconds, int nanoseconds)
+ {
+ if (nanoseconds == 0L) {
+ // 14-Mar-2015, tatu: Let's retain one zero to avoid interpretation
+ // as integral number
+ if (seconds == 0L) { // except for "0.0" where it can not be done without scientific notation
+ return BigDecimal.ZERO.setScale(1);
+ }
+ return BigDecimal.valueOf(seconds).setScale(9);
+ }
+ return NumberInput.parseBigDecimal(toDecimal(seconds, nanoseconds), false);
+ }
+
+ /**
+ * Extracts the seconds and nanoseconds component of {@code seconds} as {@code long} and {@code int}
+ * values, passing them to the given converter. The implementation avoids latency issues present
+ * on some JRE releases.
+ *
+ * @since 2.19
+ */
+ public static T extractSecondsAndNanos(BigDecimal seconds,
+ BiFunction convert, boolean negativeAdjustment) {
+ // Complexity is here to workaround unbounded latency in some BigDecimal operations.
+ // https://github.com/FasterXML/jackson-databind/issues/2141
+ long secondsOnly;
+ int nanosOnly;
+
+ BigDecimal nanoseconds = seconds.scaleByPowerOfTen(9);
+ if (nanoseconds.precision() - nanoseconds.scale() <= 0) {
+ // There are no non-zero digits to the left of the decimal point.
+ // This protects against very negative exponents.
+ secondsOnly = nanosOnly = 0;
+ }
+ else if (seconds.scale() < -63) {
+ // There would be no low-order bits once we chop to a long.
+ // This protects against very positive exponents.
+ secondsOnly = nanosOnly = 0;
+ }
+ else {
+ // Now we know that seconds has reasonable scale, we can safely chop it apart.
+ secondsOnly = seconds.longValue();
+ nanosOnly = nanoseconds.subtract(BigDecimal.valueOf(secondsOnly).scaleByPowerOfTen(9)).intValue();
+
+ if (secondsOnly < 0 && secondsOnly > Instant.MIN.getEpochSecond()) {
+ // [modules-java8#337] since 2.19, not always we need to adjust nanos
+ if (negativeAdjustment) {
+ // Issue #69 and Issue #120: avoid sending a negative adjustment to the Instant constructor, we want this as the actual nanos
+ nanosOnly = Math.abs(nanosOnly);
+ }
+ }
+ }
+
+ return convert.apply(secondsOnly, nanosOnly);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java b/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java
new file mode 100644
index 0000000000..6c85a47b90
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/util/DurationUnitConverter.java
@@ -0,0 +1,82 @@
+package tools.jackson.databind.ext.javatime.util;
+
+import static tools.jackson.databind.ext.javatime.util.DurationUnitConverter.DurationSerialization.deserializer;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Handles the conversion of the duration based on the API of {@link Duration} for a restricted set of {@link ChronoUnit}.
+ * Only the units considered as accurate are supported in this converter since are the only ones capable of handling
+ * deserialization in a precise manner (see {@link ChronoUnit#isDurationEstimated}).
+ *
+ * @since 2.12
+ */
+public class DurationUnitConverter {
+
+ protected static class DurationSerialization {
+ final Function serializer;
+ final Function deserializer;
+
+ DurationSerialization(
+ Function serializer,
+ Function deserializer) {
+ this.serializer = serializer;
+ this.deserializer = deserializer;
+ }
+
+ static Function deserializer(TemporalUnit unit) {
+ return v -> Duration.of(v, unit);
+ }
+ }
+
+ private final static Map UNITS;
+
+ static {
+ Map units = new LinkedHashMap<>();
+ units.put(ChronoUnit.NANOS.name(), new DurationSerialization(Duration::toNanos, deserializer(ChronoUnit.NANOS)));
+ units.put(ChronoUnit.MICROS.name(), new DurationSerialization(d -> d.toNanos() / 1000, deserializer(ChronoUnit.MICROS)));
+ units.put(ChronoUnit.MILLIS.name(), new DurationSerialization(Duration::toMillis, deserializer(ChronoUnit.MILLIS)));
+ units.put(ChronoUnit.SECONDS.name(), new DurationSerialization(Duration::getSeconds, deserializer(ChronoUnit.SECONDS)));
+ units.put(ChronoUnit.MINUTES.name(), new DurationSerialization(Duration::toMinutes, deserializer(ChronoUnit.MINUTES)));
+ units.put(ChronoUnit.HOURS.name(), new DurationSerialization(Duration::toHours, deserializer(ChronoUnit.HOURS)));
+ units.put(ChronoUnit.HALF_DAYS.name(), new DurationSerialization(d -> d.toHours() / 12, deserializer(ChronoUnit.HALF_DAYS)));
+ units.put(ChronoUnit.DAYS.name(), new DurationSerialization(Duration::toDays, deserializer(ChronoUnit.DAYS)));
+ UNITS = units;
+ }
+
+
+ final DurationSerialization serialization;
+
+ DurationUnitConverter(DurationSerialization serialization) {
+ this.serialization = serialization;
+ }
+
+ public Duration convert(long value) {
+ return serialization.deserializer.apply(value);
+ }
+
+ public long convert(Duration duration) {
+ return serialization.serializer.apply(duration);
+ }
+
+ /**
+ * @return Description of all allowed valued as a sequence of
+ * double-quoted values separated by comma
+ */
+ public static String descForAllowed() {
+ return "\"" + UNITS.keySet().stream()
+ .collect(Collectors.joining("\", \""))
+ + "\"";
+ }
+
+ public static DurationUnitConverter from(String unit) {
+ DurationSerialization def = UNITS.get(unit);
+ return (def == null) ? null : new DurationUnitConverter(def);
+ }
+}
diff --git a/src/main/java/tools/jackson/databind/ext/package-info.java b/src/main/java/tools/jackson/databind/ext/package-info.java
index aa8e25e823..bcafe9cbc5 100644
--- a/src/main/java/tools/jackson/databind/ext/package-info.java
+++ b/src/main/java/tools/jackson/databind/ext/package-info.java
@@ -3,21 +3,21 @@
may or may not be present in runtime environment, but that are
commonly enough used so that explicit support can be added.
-Currently supported extensions include:
+Currently included extensions are:
- Support for Java 1.5 core XML datatypes: the reason these are
-considered "external" is that some platforms that claim to be 1.5 conformant
+ Java core XML datatypes: the reason these are
+considered "external" is that some platforms that claim to be conformant
are only partially so (Google Android, GAE) and do not included these
- types.
+ types; and with Java 9 and above also due to JPMS reasons.
- Joda time. This package has superior date/time handling functionality,
-and is thus supported. However, to minimize forced dependencies this
-support is added as extension so that Joda is not needed by Jackson
-itself: but if it is present, its core types are supported to some
-degree
+ Selected {@code java.sql} types.
+
+ Selected {@code java.beans} annotations: {@code @Transient}, {@code ConstructorProperties}.
+
+ Java (8) Time (JSR-310) type support: as of Jackson 3.0 included in databind
+ but added similar to {@code JacksonModule}s for improved configurability.
-
*/
package tools.jackson.databind.ext;
diff --git a/src/main/java/tools/jackson/databind/json/JsonMapper.java b/src/main/java/tools/jackson/databind/json/JsonMapper.java
index 7bdb122bf1..5583e87722 100644
--- a/src/main/java/tools/jackson/databind/json/JsonMapper.java
+++ b/src/main/java/tools/jackson/databind/json/JsonMapper.java
@@ -4,6 +4,7 @@
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.json.JsonWriteFeature;
+
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.cfg.MapperBuilderState;
diff --git a/src/main/java/tools/jackson/databind/util/BeanUtil.java b/src/main/java/tools/jackson/databind/util/BeanUtil.java
index 523287ed3c..54118cff38 100644
--- a/src/main/java/tools/jackson/databind/util/BeanUtil.java
+++ b/src/main/java/tools/jackson/databind/util/BeanUtil.java
@@ -117,19 +117,7 @@ public static String checkUnsupportedType(MapperConfig> config, JavaType type)
final String className = type.getRawClass().getName();
String typeName, moduleName;
- if (isJava8TimeClass(className)) {
- // [modules-java8#207]: do NOT check/block helper types in sub-packages,
- // but only main-level types (to avoid issues with module)
- if (className.indexOf('.', 10) >= 0) {
- return null;
- }
- // [databind#4718]: Also don't worry about Exception type(s)
- if (type.isTypeOrSubTypeOf(Throwable.class)) {
- return null;
- }
- typeName = "Java 8 date/time";
- moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310";
- } else if (isJodaTimeClass(className)) {
+ if (isJodaTimeClass(className)) {
typeName = "Joda date/time";
moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-joda";
} else {
@@ -138,15 +126,7 @@ public static String checkUnsupportedType(MapperConfig> config, JavaType type)
return String.format("%s type %s not supported by default: add Module \"%s\" to enable handling",
typeName, ClassUtil.getTypeDescription(type), moduleName);
}
-
- public static boolean isJava8TimeClass(Class> rawType) {
- return isJava8TimeClass(rawType.getName());
- }
-
- private static boolean isJava8TimeClass(String className) {
- return className.startsWith("java.time.");
- }
-
+
public static boolean isJodaTimeClass(Class> rawType) {
return isJodaTimeClass(rawType.getName());
}
diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java
index 1126144cbb..03045f6b14 100644
--- a/src/test/java/module-info.java
+++ b/src/test/java/module-info.java
@@ -35,6 +35,14 @@
opens tools.jackson.databind.deser.jdk;
opens tools.jackson.databind.deser.std;
opens tools.jackson.databind.exc;
+ opens tools.jackson.databind.ext.javatime;
+ opens tools.jackson.databind.ext.javatime.deser;
+ opens tools.jackson.databind.ext.javatime.deser.key;
+ opens tools.jackson.databind.ext.javatime.key;
+ opens tools.jackson.databind.ext.javatime.misc;
+ opens tools.jackson.databind.ext.javatime.ser;
+ opens tools.jackson.databind.ext.javatime.tofix;
+ opens tools.jackson.databind.ext.javatime.util;
opens tools.jackson.databind.introspect;
opens tools.jackson.databind.json;
opens tools.jackson.databind.jsonFormatVisitors;
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java b/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java
new file mode 100644
index 0000000000..7611d526d2
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/DateTimeTestBase.java
@@ -0,0 +1,76 @@
+package tools.jackson.databind.ext.javatime;
+
+import java.time.ZoneId;
+import java.util.*;
+
+import tools.jackson.core.json.JsonWriteFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.databind.json.JsonMapper;
+
+public class DateTimeTestBase
+{
+ protected static final ZoneId UTC = ZoneId.of("UTC");
+
+ protected static final ZoneId Z_CHICAGO = ZoneId.of("America/Chicago");
+ protected static final ZoneId Z_BUDAPEST = ZoneId.of("Europe/Budapest");
+
+ // 14-Mar-2016, tatu: Serialization of trailing zeroes may change [datatype-jsr310#67]
+ // Note, tho, that "0.0" itself is special case; need to avoid scientific notation:
+ final protected static String NO_NANOSECS_SER = "0.0";
+ final protected static String NO_NANOSECS_SUFFIX = ".000000000";
+
+ protected static ObjectMapper newMapper() {
+ return newMapperBuilder().build();
+ }
+
+ protected static MapperBuilder,?> newMapperBuilder() {
+ return JsonMapper.builder()
+ .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES);
+ }
+
+ protected static MapperBuilder,?> newMapperBuilder(TimeZone tz) {
+ return JsonMapper.builder()
+ .defaultTimeZone(tz)
+ .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES);
+ }
+
+ protected static ObjectMapper newMapper(TimeZone tz) {
+ return newMapperBuilder(tz).build();
+ }
+
+ protected static JsonMapper.Builder mapperBuilder() {
+ return JsonMapper.builder()
+ .defaultLocale(Locale.ENGLISH)
+ .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES);
+ }
+
+ protected String q(String value) {
+ return "\"" + value + "\"";
+ }
+
+ protected String a2q(String json) {
+ return json.replace("'", "\"");
+ }
+
+ protected void verifyException(Throwable e, String... matches)
+ {
+ String msg = e.getMessage();
+ String lmsg = (msg == null) ? "" : msg.toLowerCase();
+ for (String match : matches) {
+ String lmatch = match.toLowerCase();
+ if (lmsg.indexOf(lmatch) >= 0) {
+ return;
+ }
+ }
+ throw new Error("Expected an exception with one of substrings ("+Arrays.asList(matches)+"): got one with message \""+msg+"\"");
+ }
+
+ protected static Map asMap(T key, String value) {
+ return Collections.singletonMap(key, value);
+ }
+
+ protected static String mapAsString(String key, String value) {
+ return String.format("{\"%s\":\"%s\"}", key, value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java b/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java
new file mode 100644
index 0000000000..6484240ca3
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/MockObjectConfiguration.java
@@ -0,0 +1,8 @@
+package tools.jackson.databind.ext.javatime;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY, property = "@class")
+public interface MockObjectConfiguration
+{
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java b/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java
new file mode 100644
index 0000000000..d369823a4a
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/TestDecimalUtils.java
@@ -0,0 +1,153 @@
+package tools.jackson.databind.ext.javatime;
+
+import java.math.BigDecimal;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestDecimalUtils extends DateTimeTestBase
+{
+ @Test
+ public void testToDecimal01()
+ {
+ String decimal = DecimalUtils.toDecimal(0, 0);
+ assertEquals(NO_NANOSECS_SER, decimal, "The returned decimal is not correct.");
+
+ decimal = DecimalUtils.toDecimal(15, 72);
+ assertEquals("15.000000072", decimal, "The returned decimal is not correct.");
+
+ decimal = DecimalUtils.toDecimal(19827342231L, 192837465);
+ assertEquals("19827342231.192837465", decimal, "The returned decimal is not correct.");
+
+ decimal = DecimalUtils.toDecimal(19827342231L, 0);
+ assertEquals("19827342231"+NO_NANOSECS_SUFFIX, decimal,
+ "The returned decimal is not correct.");
+
+ decimal = DecimalUtils.toDecimal(19827342231L, 999888000);
+ assertEquals("19827342231.999888000", decimal,
+ "The returned decimal is not correct.");
+
+ decimal = DecimalUtils.toDecimal(-22704862, 599000000);
+ assertEquals("-22704862.599000000", decimal,
+ "The returned decimal is not correct.");
+ }
+
+ private void checkExtractNanos(long expectedSeconds, int expectedNanos, BigDecimal decimal)
+ {
+ long seconds = decimal.longValue();
+ assertEquals(expectedSeconds, seconds, "The second part is not correct.");
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal01()
+ {
+ BigDecimal value = new BigDecimal("0");
+ checkExtractNanos(0L, 0, value);
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal02()
+ {
+ BigDecimal value = new BigDecimal("15.000000072");
+ checkExtractNanos(15L, 72, value);
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal03()
+ {
+ BigDecimal value = new BigDecimal("15.72");
+ checkExtractNanos(15L, 720000000, value);
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal04()
+ {
+ BigDecimal value = new BigDecimal("19827342231.192837465");
+ checkExtractNanos(19827342231L, 192837465, value);
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal05()
+ {
+ BigDecimal value = new BigDecimal("19827342231");
+ checkExtractNanos(19827342231L, 0, value);
+ }
+
+ @Test
+ public void testExtractNanosecondDecimal06()
+ {
+ BigDecimal value = new BigDecimal("19827342231.999999999");
+ checkExtractNanos(19827342231L, 999999999, value);
+ }
+
+ private void checkExtractSecondsAndNanos(long expectedSeconds, int expectedNanos, BigDecimal decimal)
+ {
+ DecimalUtils.extractSecondsAndNanos(decimal, (Long s, Integer ns) -> {
+ assertEquals(expectedSeconds, s.longValue(), "The second part is not correct.");
+ assertEquals(expectedNanos, ns.intValue(), "The nanosecond part is not correct.");
+ return null;
+ }, true);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos01()
+ {
+ BigDecimal value = new BigDecimal("0");
+ checkExtractSecondsAndNanos(0L, 0, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos02()
+ {
+ BigDecimal value = new BigDecimal("15.000000072");
+ checkExtractSecondsAndNanos(15L, 72, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos03()
+ {
+ BigDecimal value = new BigDecimal("15.72");
+ checkExtractSecondsAndNanos(15L, 720000000, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos04()
+ {
+ BigDecimal value = new BigDecimal("19827342231.192837465");
+ checkExtractSecondsAndNanos(19827342231L, 192837465, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos05()
+ {
+ BigDecimal value = new BigDecimal("19827342231");
+ checkExtractSecondsAndNanos(19827342231L, 0, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanos06()
+ {
+ BigDecimal value = new BigDecimal("19827342231.999999999");
+ checkExtractSecondsAndNanos(19827342231L, 999999999, value);
+ }
+
+ @Test
+ public void testExtractSecondsAndNanosFromNegativeBigDecimal()
+ {
+ BigDecimal value = new BigDecimal("-22704862.599000000");
+ checkExtractSecondsAndNanos(-22704862L, 599000000, value);
+ }
+
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testExtractSecondsAndNanos07()
+ {
+ BigDecimal value = new BigDecimal("1e10000000");
+ checkExtractSecondsAndNanos(0L, 0, value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java
new file mode 100644
index 0000000000..ff0fd25638
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.SerializationFeature;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestFeatures
+{
+ @Test
+ public void testWriteDateTimestampsAsNanosecondsSettingEnabledByDefault()
+ {
+ assertTrue(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS.enabledByDefault(),
+ "Write date timestamps as nanoseconds setting should be enabled by default.");
+ }
+
+ @Test
+ public void testReadDateTimestampsAsNanosecondsSettingEnabledByDefault()
+ {
+ assertTrue(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS.enabledByDefault(),
+ "Read date timestamps as nanoseconds setting should be enabled by default.");
+ }
+
+ @Test
+ public void testAdjustDatesToContextTimeZoneSettingEnabledByDefault()
+ {
+ assertTrue(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE.enabledByDefault(),
+ "Adjust dates to context time zone setting should be enabled by default.");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java
new file mode 100644
index 0000000000..df6fb8c1c2
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DefaultTypingTest.java
@@ -0,0 +1,51 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.ZoneId;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.DatabindContext;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DefaultTypingTest extends DateTimeTestBase
+{
+ static class NoCheckSubTypeValidator
+ extends PolymorphicTypeValidator.Base
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Validity validateBaseType(DatabindContext ctxt, JavaType baseType) {
+ return Validity.ALLOWED;
+ }
+ }
+
+ private final ObjectMapper TYPING_MAPPER = newMapperBuilder()
+ .activateDefaultTyping(new NoCheckSubTypeValidator())
+ .build();
+
+ // for [datatype-jsr310#24]
+ @Test
+ public void testZoneIdAsIs() throws Exception
+ {
+ ZoneId exp = ZoneId.of("America/Chicago");
+ String json = TYPING_MAPPER.writeValueAsString(exp);
+ ZoneId act = TYPING_MAPPER.readValue(json, ZoneId.class);
+ assertEquals(exp, act);
+ }
+
+ // This one WILL add type info, since `ZoneId` is abstract type:
+ @Test
+ public void testZoneWithForcedBaseType() throws Exception
+ {
+ ZoneId exp = ZoneId.of("America/Chicago");
+ String json = TYPING_MAPPER.writerFor(ZoneId.class).writeValueAsString(exp);
+ ZoneId act = TYPING_MAPPER.readValue(json, ZoneId.class);
+ assertEquals(exp, act);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java
new file mode 100644
index 0000000000..b11a5fffd8
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeser337Test.java
@@ -0,0 +1,48 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class DurationDeser337Test extends DateTimeTestBase
+{
+ @Test
+ public void testWithDurationsAsTimestamps() throws Exception
+ {
+ final ObjectMapper MAPPER_DURATION_TIMESTAMPS = mapperBuilder()
+ .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .build();
+
+ Duration duration = Duration.parse("PT-43.636S");
+
+ String ser = MAPPER_DURATION_TIMESTAMPS.writeValueAsString(duration);
+
+ assertEquals("-43.636000000", ser);
+
+ Duration deser = MAPPER_DURATION_TIMESTAMPS.readValue(ser, Duration.class);
+
+ assertEquals(duration, deser);
+ assertEquals(deser.toString(), "PT-43.636S");
+ }
+
+ @Test
+ public void testWithoutDurationsAsTimestamps() throws Exception
+ {
+ ObjectMapper mapper = mapperBuilder()
+ .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .build();
+
+ Duration duration = Duration.parse("PT-43.636S");
+
+ String ser = mapper.writeValueAsString(duration);
+ assertEquals(q("PT-43.636S"), ser);
+
+ Duration deser = mapper.readValue(ser, Duration.class);
+ assertEquals(duration, deser);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java
new file mode 100644
index 0000000000..d6a76f7e62
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/DurationDeserTest.java
@@ -0,0 +1,585 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.math.BigInteger;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAmount;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.InvalidDefinitionException;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DurationDeserTest extends DateTimeTestBase
+{
+ private final ObjectReader READER = newMapper().readerFor(Duration.class);
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ final static class Wrapper {
+ public Duration value;
+
+ public Wrapper() { }
+ public Wrapper(Duration v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public Duration value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(Duration v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public Duration value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(Duration v) { value = v; }
+ }
+
+
+ @Test
+ public void testDeserializationAsFloat01() throws Exception
+ {
+ Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("60.0");
+ assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat02() throws Exception
+ {
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("60.0");
+ assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat03() throws Exception
+ {
+ Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("13498.000008374");
+ assertEquals(Duration.ofSeconds(13498L, 8374), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat04() throws Exception
+ {
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("13498.000008374");
+ assertEquals(Duration.ofSeconds(13498L, 8374), value, "The value is not correct.");
+ }
+
+ /**
+ * Test the upper-bound of Duration.
+ */
+ @Test
+ public void testDeserializationAsFloatEdgeCase01() throws Exception
+ {
+ String input = Long.MAX_VALUE + ".999999999";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(Long.MAX_VALUE, value.getSeconds());
+ assertEquals(999999999, value.getNano());
+ }
+
+ /**
+ * Test the lower-bound of Duration.
+ */
+ @Test
+ public void testDeserializationAsFloatEdgeCase02() throws Exception
+ {
+ String input = Long.MIN_VALUE + ".0";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(Long.MIN_VALUE, value.getSeconds());
+ assertEquals(0, value.getNano());
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase03() throws Exception
+ {
+ // Duration can't go this low
+ assertThrows(ArithmeticException.class, () -> {
+ READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.MIN_VALUE + ".1");
+ });
+ }
+
+ /*
+ * DurationDeserializer currently uses BigDecimal.longValue() which has surprising behavior
+ * for numbers outside the range of Long. Numbers less than 1e64 will result in the lower 64 bits.
+ * Numbers at or above 1e64 will always result in zero.
+ */
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase04() throws Exception
+ {
+ // Just beyond the upper-bound of Duration.
+ String input = new BigInteger(Long.toString(Long.MAX_VALUE)).add(BigInteger.ONE) + ".0";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(Long.MIN_VALUE, value.getSeconds()); // We've turned a positive number into negative duration!
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase05() throws Exception
+ {
+ // Just beyond the lower-bound of Duration.
+ String input = new BigInteger(Long.toString(Long.MIN_VALUE)).subtract(BigInteger.ONE) + ".0";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(Long.MAX_VALUE, value.getSeconds()); // We've turned a negative number into positive duration!
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase06() throws Exception
+ {
+ // Into the positive zone where everything becomes zero.
+ String input = "1e64";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase07() throws Exception
+ {
+ // Into the negative zone where everything becomes zero.
+ String input = "-1e64";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ /**
+ * Numbers with very large exponents can take a long time, but still result in zero.
+ * https://github.com/FasterXML/jackson-databind/issues/2141
+ */
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase08() throws Exception
+ {
+ String input = "1e10000000";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase09() throws Exception
+ {
+ String input = "-1e10000000";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ /**
+ * Same for large negative exponents.
+ */
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase10() throws Exception
+ {
+ String input = "1e-10000000";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase11() throws Exception
+ {
+ String input = "-1e-10000000";
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(input);
+ assertEquals(0, value.getSeconds());
+ }
+
+ @Test
+ public void testDeserializationAsInt01() throws Exception
+ {
+ Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("60");
+ assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02() throws Exception
+ {
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("60000");
+ assertEquals(Duration.ofSeconds(60L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt03() throws Exception
+ {
+ Duration value = READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("13498");
+ assertEquals(Duration.ofSeconds(13498L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt04() throws Exception
+ {
+ Duration value = READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("13498000");
+ assertEquals(Duration.ofSeconds(13498L, 0), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt05() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ WrapperWithReadTimestampsAsNanosEnabled expected =
+ new WrapperWithReadTimestampsAsNanosEnabled(Duration.ofSeconds(13498L, 0));
+ WrapperWithReadTimestampsAsNanosEnabled actual =
+ reader.readValue(wrapperPayload(13498));
+ assertEquals(expected.value, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt06() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ WrapperWithReadTimestampsAsNanosDisabled expected =
+ new WrapperWithReadTimestampsAsNanosDisabled(Duration.ofSeconds(13498L, 0));
+ WrapperWithReadTimestampsAsNanosDisabled actual =
+ reader.readValue(wrapperPayload(13498000));
+ assertEquals(expected.value, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString01() throws Exception
+ {
+ Duration exp = Duration.ofSeconds(60L, 0);
+ Duration value = READER.readValue('"' + exp.toString() + '"');
+ assertEquals(exp, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02() throws Exception
+ {
+ Duration exp = Duration.ofSeconds(13498L, 8374);
+ Duration value = READER.readValue('"' + exp.toString() + '"');
+ assertEquals(exp, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString03() throws Exception
+ {
+ assertNull(READER.readValue("\" \""), "The value should be null.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+
+ String prefix = "[\"" + Duration.class.getName() + "\",";
+
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ TemporalAmount value = mapper.readerFor(TemporalAmount.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(prefix + "13498.000008374]");
+
+ assertTrue(value instanceof Duration, "The value should be a Duration.");
+ assertEquals(duration, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02() throws Exception
+ {
+ String prefix = "[\"" + Duration.class.getName() + "\",";
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ TemporalAmount value = mapper.readerFor(TemporalAmount.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(prefix + "13498]");
+ assertTrue(value instanceof Duration, "The value should be a Duration.");
+ assertEquals(Duration.ofSeconds(13498L), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ String prefix = "[\"" + Duration.class.getName() + "\",";
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ TemporalAmount value = mapper
+ .readerFor(TemporalAmount.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(prefix + "13498837]");
+ assertTrue(value instanceof Duration, "The value should be a Duration.");
+ assertEquals(Duration.ofSeconds(13498L, 837000000), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo04() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+ String prefix = "[\"" + Duration.class.getName() + "\",";
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ TemporalAmount value = mapper.readerFor(TemporalAmount.class)
+ .readValue(prefix + '"' + duration.toString() + "\"]");
+ assertTrue(value instanceof Duration, "The value should be a Duration.");
+ assertEquals(duration, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Exception {
+ Duration exp = Duration.ofSeconds(13498L, 8374);
+ try {
+ READER.readValue("[\"" + exp.toString() + "\"]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value");
+ }
+ try {
+ READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Duration` from Array value");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Exception {
+ Duration exp = Duration.ofSeconds(13498L, 8374);
+ Duration value = newMapper().readerFor(Duration.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[\"" + exp.toString() + "\"]");
+ assertEquals(exp, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ Duration value = newMapper().readerFor(Duration.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "duration";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsNullStr = null;
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ Duration actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ Duration actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "duration";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(Duration.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+ final String dateValAsNullStr = null;
+
+ // even with strict, null value should be deserialized without throwing an exception
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String dateValAsEmptyStr = "";
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for custom patterns (modules-java8#184)
+ /**********************************************************
+ */
+
+ @Test
+ public void shouldDeserializeInNanos_whenNanosUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("NANOS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+ assertEquals(Duration.ofNanos(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInMicros_whenMicrosUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("MICROS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.of(25, ChronoUnit.MICROS), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInMillis_whenMillisUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("MILLIS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.ofMillis(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInSeconds_whenSecondsUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("SECONDS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.ofSeconds(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInMinutes_whenMinutesUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.ofMinutes(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInHours_whenHoursUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("HOURS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.ofHours(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInHalfDays_whenHalfDaysUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.of(25, ChronoUnit.HALF_DAYS), wrapper.value);
+ }
+
+ @Test
+ public void shouldDeserializeInDays_whenDaysUnitAsPattern_andValueIsInteger() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("DAYS");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25));
+
+ assertEquals(Duration.ofDays(25), wrapper.value);
+ }
+
+ @Test
+ public void shouldIgnoreUnitPattern_whenValueIsFloat() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue(wrapperPayload(25.5));
+
+ assertEquals(Duration.parse("PT25.5S"), wrapper.value);
+ }
+
+ @Test
+ public void shouldIgnoreUnitPattern_whenValueIsString() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ Wrapper wrapper = reader.readValue("{\"value\":\"PT25S\"}");
+
+ assertEquals(Duration.parse("PT25S"), wrapper.value);
+ }
+
+ @Test
+ public void shouldFailForInvalidPattern() throws Exception {
+ ObjectMapper mapper = _mapperForPatternOverride("Nanos");
+ ObjectReader reader = mapper.readerFor(Wrapper.class);
+
+ try {
+ /*Wrapper wrapper =*/ reader.readValue(wrapperPayload(25));
+ fail("Should not allow invalid 'pattern'");
+ } catch (InvalidDefinitionException e) {
+ verifyException(e, "Bad 'pattern' definition (\"Nanos\")");
+ verifyException(e, "expected one of [");
+ }
+ }
+
+ private String wrapperPayload(Number number) {
+ return "{\"value\":" + number + "}";
+ }
+
+ private ObjectMapper _mapperForPatternOverride(String patternStr) {
+ return mapperBuilder()
+ .withConfigOverride(Duration.class,
+ o -> o.setFormat(JsonFormat.Value.forPattern(patternStr)))
+ .build();
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java
new file mode 100644
index 0000000000..d9e3b49da9
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeser291Test.java
@@ -0,0 +1,51 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Instant;
+import java.util.Locale;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// [modules-java8#291] InstantDeserializer fails to parse negative numeric timestamp strings for
+// pre-1970 values.
+public class InstantDeser291Test
+ extends DateTimeTestBase
+{
+ private final JsonMapper MAPPER = JsonMapper.builder()
+ .defaultLocale(Locale.ENGLISH)
+ .enable(DateTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS)
+ .build();
+ private final ObjectReader READER = MAPPER.readerFor(Instant.class);
+
+ private static final Instant INSTANT_3_SEC_AFTER_EPOC = Instant.ofEpochSecond(3);
+ private static final Instant INSTANT_3_SEC_BEFORE_EPOC = Instant.ofEpochSecond(-3);
+
+ private static final String STR_3_SEC = "\"3.000000000\"";
+ private static final String STR_POSITIVE_3 = "\"+3.000000000\"";
+ private static final String STR_NEGATIVE_3 = "\"-3.000000000\"";
+
+ /**
+ * Baseline that always succeeds, even before resolution of issue 291
+ * @throws Exception
+ */
+ @Test
+ public void testNormalNumericalString() throws Exception {
+ assertEquals(INSTANT_3_SEC_AFTER_EPOC, READER.readValue(STR_3_SEC));
+ }
+
+ @Test
+ public void testNegativeNumericalString() throws Exception {
+ assertEquals(INSTANT_3_SEC_BEFORE_EPOC, READER.readValue(STR_NEGATIVE_3));
+ }
+
+ @Test
+ public void testAllowedPlusSignNumericalString() throws Exception {
+ assertEquals(INSTANT_3_SEC_AFTER_EPOC, READER.readValue(STR_POSITIVE_3));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java
new file mode 100644
index 0000000000..fe77ce6633
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/InstantDeserTest.java
@@ -0,0 +1,654 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.SerializationFeature;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static tools.jackson.databind.ext.javatime.deser.InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX;
+
+public class InstantDeserTest extends DateTimeTestBase
+{
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT;
+ private static final String CUSTOM_PATTERN = "yyyy-MM-dd HH:mm:ss";
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ static class Wrapper {
+ @JsonFormat(
+ // 22-Jun-2015, tatu: I'll be damned if I understand why pattern does not
+ // work here... but it doesn't. Someone with better date-fu has to come
+ // and fix this; until then I will only verify that we can force textual
+ // representation here
+ //pattern="YYYY-mm-dd",
+ shape=JsonFormat.Shape.STRING
+ )
+ public Instant value;
+
+ public Wrapper() { }
+ public Wrapper(Instant v) { value = v; }
+ }
+
+ static class WrapperWithCustomPattern {
+ @JsonFormat(
+ pattern = CUSTOM_PATTERN,
+ shape=JsonFormat.Shape.STRING,
+ timezone = "UTC"
+ )
+ public Instant valueInUTC;
+
+ public WrapperWithCustomPattern() { }
+ public WrapperWithCustomPattern(Instant v) {
+ valueInUTC = v;
+ }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public Instant value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(Instant v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=JsonFormat.Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public Instant value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(Instant v) { value = v; }
+ }
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(Instant.class);
+
+ /*
+ /**********************************************************************
+ /* Basic deserialization from floating point value (seconds with fractions)
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationAsFloat01() throws Exception {
+ assertEquals(Instant.ofEpochSecond(0L),
+ READER.readValue("0.000000000"));
+ }
+
+ @Test
+ public void testDeserializationAsFloat02() throws Exception {
+ assertEquals(Instant.ofEpochSecond(123456789L, 183917322),
+ READER.readValue("123456789.183917322"));
+ }
+
+ @Test
+ public void testDeserializationAsFloat03() throws Exception
+ {
+ Instant date = Instant.now();
+ Instant value = READER.readValue(
+ DecimalUtils.toDecimal(date.getEpochSecond(), date.getNano()));
+ assertEquals(date, value);
+ }
+
+ /**
+ * Test the upper-bound of Instant.
+ */
+ @Test
+ public void testDeserializationAsFloatEdgeCase01() throws Exception
+ {
+ String input = Instant.MAX.getEpochSecond() + ".999999999";
+ Instant value = READER.readValue(input);
+ assertEquals(value, Instant.MAX);
+ assertEquals(Instant.MAX.getEpochSecond(), value.getEpochSecond());
+ assertEquals(999999999, value.getNano());
+ }
+
+ /**
+ * Test the lower-bound of Instant.
+ */
+ @Test
+ public void testDeserializationAsFloatEdgeCase02() throws Exception
+ {
+ String input = Instant.MIN.getEpochSecond() + ".0";
+ Instant value = READER.readValue(input);
+ assertEquals(value, Instant.MIN);
+ assertEquals(Instant.MIN.getEpochSecond(), value.getEpochSecond());
+ assertEquals(0, value.getNano());
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase03() throws Exception
+ {
+ // Instant can't go this low
+ String input = Instant.MIN.getEpochSecond() + ".1";
+ assertThrows(DateTimeException.class, () -> READER.readValue(input));
+ }
+
+ /*
+ * InstantDeserializer currently uses BigDecimal.longValue() which has surprising behavior
+ * for numbers outside the range of Long. Numbers less than 1e64 will result in the lower 64 bits.
+ * Numbers at or above 1e64 will always result in zero.
+ */
+ @Test
+ public void testDeserializationAsFloatEdgeCase04() throws Exception
+ {
+ // 1ns beyond the upper-bound of Instant.
+ String input = (Instant.MAX.getEpochSecond() + 1) + ".0";
+ assertThrows(DateTimeException.class, () -> READER.readValue(input));
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase05() throws Exception
+ {
+ // 1ns beyond the lower-bound of Instant.
+ String input = (Instant.MIN.getEpochSecond() - 1) + ".0";
+ assertThrows(DateTimeException.class, () -> READER.readValue(input));
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase06() throws Exception
+ {
+ // Into the positive zone where everything becomes zero.
+ Instant value = READER.readValue("1e64");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ @Test
+ public void testDeserializationAsFloatEdgeCase07() throws Exception
+ {
+ // Into the negative zone where everything becomes zero.
+ Instant value = READER.readValue("-1e64");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ /**
+ * Numbers with very large exponents can take a long time, but still result in zero.
+ * https://github.com/FasterXML/jackson-databind/issues/2141
+ */
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase08() throws Exception
+ {
+ Instant value = READER.readValue("1e10000000");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase09() throws Exception
+ {
+ Instant value = READER.readValue("-1e10000000");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ /**
+ * Same for large negative exponents.
+ */
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase10() throws Exception
+ {
+ Instant value = READER.readValue("1e-10000000");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
+ @Test
+ public void testDeserializationAsFloatEdgeCase11() throws Exception
+ {
+ Instant value = READER.readValue("-1e-10000000");
+ assertEquals(0, value.getEpochSecond());
+ }
+
+ /*
+ /**********************************************************************
+ /* Basic deserialization from Integer (long) value, as nanos
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationAsInt01Nanoseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ Instant value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("0");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt02Nanoseconds() throws Exception
+ {
+ final long ts = 123456789L;
+ Instant date = Instant.ofEpochSecond(ts);
+ Instant value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(String.valueOf(ts));
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt03Nanoseconds() throws Exception
+ {
+ Instant date = Instant.now();
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+
+ Instant value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.getEpochSecond()));
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt04Nanoseconds() throws Exception
+ {
+ ObjectReader reader = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ Instant date = Instant.now();
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+ WrapperWithReadTimestampsAsNanosEnabled expected =
+ new WrapperWithReadTimestampsAsNanosEnabled(date);
+ WrapperWithReadTimestampsAsNanosEnabled actual = reader.readValue(
+ a2q("{'value':" + date.getEpochSecond() + "}"));
+ assertEquals(expected.value, actual.value);
+ }
+
+ /*
+ /**********************************************************************
+ /* Basic deserialization from Integer (long) value, as milliseconds
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationAsInt01Milliseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ Instant value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("0");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt02Milliseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 422000000);
+ Instant value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("123456789422");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt03Milliseconds() throws Exception
+ {
+ Instant date = Instant.now();
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+
+ Instant value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.toEpochMilli()));
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsInt04Milliseconds() throws Exception
+ {
+ ObjectReader reader = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ Instant date = Instant.now();
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+ WrapperWithReadTimestampsAsNanosDisabled expected =
+ new WrapperWithReadTimestampsAsNanosDisabled(date);
+ WrapperWithReadTimestampsAsNanosDisabled actual = reader.readValue(
+ a2q("{'value':" + date.toEpochMilli() + "}"));
+ assertEquals(expected.value, actual.value);
+ }
+
+ /*
+ /**********************************************************************
+ /* Basic deserialization from String (ISO-8601 timestamps)
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationAsString01() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ Instant value = READER.readValue('"' + FORMATTER.format(date) + '"');
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsString02() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ Instant value = READER.readValue('"' + FORMATTER.format(date) + '"');
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationAsString03() throws Exception
+ {
+ Instant date = Instant.now();
+
+ Instant value = READER.readValue('"' + FORMATTER.format(date) + '"');
+ assertEquals(date, value);
+ }
+
+ /*
+ /**********************************************************************
+ /* Polymorphic deserialization, numeric timestamps
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + Instant.class.getName() + "\",123456789.183917322]", Temporal.class
+ );
+ assertTrue(value instanceof Instant, "The value should be an Instant.");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 0);
+ ObjectMapper m = newMapperBuilder()
+ .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + Instant.class.getName() + "\",123456789]", Temporal.class
+ );
+ assertTrue(value instanceof Instant, "The value should be an Instant.");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 422000000);
+ ObjectMapper m = newMapperBuilder()
+ .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + Instant.class.getName() + "\",123456789422]", Temporal.class
+ );
+
+ assertTrue(value instanceof Instant, "The value should be an Instant.");
+ assertEquals(date, value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo04() throws Exception
+ {
+ Instant date = Instant.now();
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + Instant.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class
+ );
+ assertTrue(value instanceof Instant, "The value should be an Instant.");
+ assertEquals(date, value);
+ }
+
+ /*
+ /**********************************************************************
+ /* Deserialization with custom pattern overrides (for String values)
+ /**********************************************************************
+ */
+
+ @Test
+ public void testCustomPatternWithAnnotations01() throws Exception
+ {
+ final Wrapper input = new Wrapper(Instant.ofEpochMilli(0));
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals(a2q("{'value':'1970-01-01T00:00:00Z'}"), json);
+
+ Wrapper result = MAPPER.readValue(json, Wrapper.class);
+ assertEquals(input.value, result.value);
+ }
+
+ // [datatype-jsr310#69]
+ @Test
+ public void testCustomPatternWithAnnotations02() throws Exception
+ {
+ //Test date is pushed one year after start of the epoch just to avoid possible issues with UTC-X TZs which could
+ //push the instant before tha start of the epoch
+ final Instant instant = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneOffset.UTC).plusYears(1).toInstant();
+ final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(CUSTOM_PATTERN);
+ final String valueInUTC = formatter.withZone(ZoneOffset.UTC).format(instant);
+
+ final WrapperWithCustomPattern input = new WrapperWithCustomPattern(instant);
+ String json = MAPPER.writeValueAsString(input);
+
+ assertTrue(json.contains(a2q("'valueInUTC':'" + valueInUTC + "'")),
+ "Instant in UTC timezone was not serialized as expected.");
+
+ WrapperWithCustomPattern result = MAPPER.readValue(json, WrapperWithCustomPattern.class);
+ assertEquals(input.valueInUTC, result.valueInUTC,
+ "Instant in UTC timezone was not deserialized as expected.");
+ }
+
+ /*
+ /**********************************************************************
+ /* Deserialization, timezone overrides
+ /**********************************************************************
+ */
+
+ // [jackson-modules-java8#18]
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset01() throws Exception {
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "+00:00");
+ Instant result = READER.readValue(json);
+ assertEquals(date, result);
+ }
+
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset02() throws Exception {
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "+0000");
+ Instant result = READER.readValue(json);
+ assertEquals(date, result);
+ }
+
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset03() throws Exception {
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "+00");
+ Instant result = READER.readValue(json);
+ assertEquals(date, result);
+ }
+
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset04() throws Exception {
+ assumeInstantCanParseOffsets();
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "+00:30");
+ Instant result = READER.readValue(json);
+ assertNotEquals(date, result);
+ }
+
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset05() throws Exception {
+ assumeInstantCanParseOffsets();
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "+01:30");
+ Instant result = READER.readValue(json);
+ assertNotEquals(date, result);
+ }
+
+ @Test
+ public void testDeserializationFromStringWithZeroZoneOffset06() throws Exception {
+ assumeInstantCanParseOffsets();
+ Instant date = Instant.now();
+ String json = formatWithZeroZoneOffset(date, "-00:00");
+ Instant result = READER.readValue(json);
+ assertEquals(date, result);
+ }
+
+ private String formatWithZeroZoneOffset(Instant date, String offset){
+ return '"' + FORMATTER.format(date).replaceFirst("Z$", offset) + '"';
+ }
+
+ private static void assumeInstantCanParseOffsets() {
+ // 27-Jan-2025, tatu: We are on JDK 17 so true always
+ // DateTimeFormatter.ISO_INSTANT didn't handle offsets until JDK 12+.
+ ;
+ }
+
+ /*
+ /**********************************************************************
+ /* Deserialization, misc other
+ /**********************************************************************
+ */
+
+ // [datatype-jsr310#16]
+ @Test
+ public void testDeserializationFromStringAsNumber() throws Exception
+ {
+ // First, baseline test with floating-point numbers
+ Instant inst = Instant.now();
+ String json = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(inst);
+ Instant result = READER.readValue(json);
+ assertNotNull(result);
+ assertEquals(result, inst);
+
+ // but then quoted as JSON String
+ result = READER.readValue(String.format("\"%s\"", json));
+ assertNotNull(result);
+ assertEquals(result, inst);
+ }
+
+ // [datatype-jsr310#79]
+ @Test
+ public void testRoundTripOfInstantAndJavaUtilDate() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .build();
+
+ Instant givenInstant = LocalDate.of(2016, 1, 1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant();
+ String json = mapper.writeValueAsString(java.util.Date.from(givenInstant));
+ Instant actual = mapper.readValue(json, Instant.class);
+
+ assertEquals(givenInstant, actual);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "duration";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsNullStr = null;
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ Duration actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ Duration actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "instant";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(Instant.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ /*
+ /************************************************************************
+ /* Tests for InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX
+ /************************************************************************
+ */
+ @Test
+ public void testISO8601ColonlessRegexFindsOffset() {
+ Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100");
+
+ assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset");
+ assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset");
+ }
+
+ @Test
+ public void testISO8601ColonlessRegexFindsOffsetWithTZ() {
+ Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100[Europe/Paris]");
+
+ assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset");
+ assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset");
+ }
+
+ @Test
+ public void testISO8601ColonlessRegexDoesNotAffectNegativeYears() {
+ Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+01:00[Europe/Paris]");
+
+ assertFalse(matcher.find(), "Matcher does not find -2000 (years) as an offset without colon");
+ }
+
+ @Test
+ public void testISO8601ColonlessRegexDoesNotAffectNegativeYearsWithColonless() {
+ Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+0100[Europe/Paris]");
+
+ assertTrue(matcher.find(), "Matcher finds +0100 as an colonless offset");
+ assertEquals(matcher.group(), "+0100", "Matcher groups +0100 as an colonless offset");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java
new file mode 100644
index 0000000000..dd7f902374
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateDeserTest.java
@@ -0,0 +1,600 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.*;
+import java.time.temporal.Temporal;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import com.fasterxml.jackson.annotation.OptBoolean;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(LocalDate.class);
+ private final ObjectReader READER_USING_TIME_ZONE = JsonMapper.builder()
+ .enable(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)
+ .build()
+ .readerFor(LocalDate.class);
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ final static class Wrapper {
+ @JsonFormat(pattern="yyyy_MM_dd'T'HH:mmZ",
+ shape=JsonFormat.Shape.STRING)
+ public LocalDate value;
+
+ public Wrapper() { }
+ public Wrapper(LocalDate v) { value = v; }
+ }
+
+ final static class ShapeWrapper {
+ @JsonFormat(shape=JsonFormat.Shape.NUMBER_INT)
+ public LocalDate date;
+
+ public ShapeWrapper() { }
+ public ShapeWrapper(LocalDate v) { date = v; }
+ }
+
+ static class StrictWrapperWithFormat {
+ @JsonFormat(pattern="yyyy-MM-dd",
+ lenient = OptBoolean.FALSE)
+ public LocalDate value;
+
+ public StrictWrapperWithFormat() { }
+ public StrictWrapperWithFormat(LocalDate v) { value = v; }
+ }
+
+ final static class StrictWrapperWithYearOfEra {
+ @JsonFormat(pattern="yyyy-MM-dd G",
+ lenient = OptBoolean.FALSE)
+ public LocalDate value;
+
+ public StrictWrapperWithYearOfEra() { }
+ public StrictWrapperWithYearOfEra(LocalDate v) { value = v; }
+ }
+
+ final static class StrictWrapperWithYearWithoutEra {
+ @JsonFormat(pattern="uuuu-MM-dd",
+ lenient = OptBoolean.FALSE)
+ public LocalDate value;
+
+ public StrictWrapperWithYearWithoutEra() { }
+ public StrictWrapperWithYearWithoutEra(LocalDate v) { value = v; }
+ }
+
+ /*
+ /**********************************************************
+ /* Deserialization from Int array representation
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsTimestamp01()
+ {
+ assertEquals(LocalDate.of(1986, Month.JANUARY, 17),
+ READER.readValue("[1986,1,17]"));
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp02()
+ {
+ assertEquals(LocalDate.of(2013, Month.AUGUST, 21),
+ READER.readValue("[2013,8,21]"));
+ }
+
+ /*
+ /**********************************************************
+ /* Deserialization from String representation
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsString01()
+ {
+ assertEquals(LocalDate.of(2000, Month.JANUARY, 1), READER.readValue(q("2000-01-01")));
+
+ LocalDate date = LocalDate.of(1986, Month.JANUARY, 17);
+ assertEquals(date, READER.readValue('"' + date.toString() + '"'));
+
+ date = LocalDate.of(2013, Month.AUGUST, 21);
+ assertEquals(date, READER.readValue('"' + date.toString() + '"'));
+ }
+
+ @Test
+ public void testDeserializationAsString02()
+ {
+ LocalDateTime date = LocalDateTime.now();
+ assertEquals(date.toLocalDate(), READER.readValue('"' + date.toString() + '"'));
+ }
+
+ @Test
+ public void testLenientDeserializationAsString01() throws Exception
+ {
+ Instant instant = Instant.now();
+ LocalDate value = READER.readValue(q(instant.toString()));
+ assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
+ }
+
+ @Test
+ public void testLenientDeserializationAsString02() throws Exception
+ {
+ ObjectReader reader = READER.with(TimeZone.getTimeZone(Z_BUDAPEST));
+ Instant instant = Instant.now();
+ LocalDate value = reader.readValue(q(instant.toString()));
+ assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
+ }
+
+ @Test
+ public void testLenientDeserializationAsString03() throws Exception
+ {
+ Instant instant = Instant.now();
+ LocalDate value = READER_USING_TIME_ZONE.readValue(q(instant.toString()));
+ assertEquals(LocalDateTime.ofInstant(instant, ZoneOffset.UTC).toLocalDate(), value);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "Europe/Budapest, 2024-07-21T21:59:59Z, 2024-07-21",
+ "Europe/Budapest, 2024-07-21T22:00:00Z, 2024-07-22",
+ "America/Chicago, 2024-07-22T04:59:59Z, 2024-07-21",
+ "America/Chicago, 2024-07-22T05:00:00Z, 2024-07-22"
+ })
+ public void testLenientDeserializationAsString04(TimeZone zone, String string, LocalDate expected) throws Exception
+ {
+ ObjectReader reader = READER_USING_TIME_ZONE.with(zone);
+ LocalDate value = reader.readValue(q(string));
+ assertEquals(expected, value);
+ }
+
+ @Test
+ public void testBadDeserializationAsString01()
+ {
+ try {
+ READER.readValue(q("notalocaldate"));
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type");
+ verifyException(e, "from String \"");
+ }
+ }
+
+ @Test
+ public void testBadDeserializationAsString02()
+ {
+ try {
+ READER.readValue(q("2015-06-19TShouldNotParse"));
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type");
+ verifyException(e, "from String \"");
+ }
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01()
+ {
+ ObjectMapper mapper = mapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5);
+ Temporal value = mapper.readValue(
+ "[\"" + LocalDate.class.getName() + "\",\"" + date.toString() + "\"]", Temporal.class
+ );
+ assertEquals(date, value);
+ }
+
+ /*
+ /**********************************************************
+ /* Deserialization from alternate representation: int (number
+ /* of days since Epoch)
+ /**********************************************************
+ */
+
+ // By default, lenient handling on so we can do this:
+ @Test
+ public void testLenientDeserializeFromInt()
+ {
+ assertEquals(LocalDate.of(1970, Month.JANUARY, 3), READER.readValue("2"));
+
+ assertEquals(LocalDate.of(1970, Month.FEBRUARY, 10), READER.readValue("40"));
+ }
+
+ // But with alternate setting, not so
+ @Test
+ public void testStricDeserializeFromInt()
+ {
+ ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ c -> c.setFormat(JsonFormat.Value.forLeniency(false))
+ )
+ .build();
+ try {
+ mapper.readValue("2", LocalDate.class);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize instance of");
+ verifyException(e, "not allowed because 'strict' mode set for property or type");
+ }
+
+ // 17-Aug-2019, tatu: Should possibly test other mechanism too, but for now let's
+ // be content with just one...
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString()
+ {
+
+ String key = "date";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsNullStr = null;
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ LocalDate actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ LocalDate actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(actualDateFromNullStr, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ // ( expected = MismatchedInputException.class)
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "date";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ c -> c.setFormat(JsonFormat.Value.forLeniency(false))
+ )
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+ final String dateValAsNullStr = null;
+
+ // even with strict, null value should be deserialized without throwing an exception
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String dateValAsEmptyStr = "";
+ // TODO: nothing stops us from writing an empty string, maybe there should be a check there too?
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", dateValAsEmptyStr));
+ // with strict, deserializing an empty string is not permitted
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for alternate array handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsArrayDisabled()
+ {
+ try {
+ READER.readValue("[\"2000-01-01\"]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Unexpected token (VALUE_STRING) within Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled()
+ {
+ // works even without the feature enabled
+ assertNull(READER.readValue("[]"));
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled()
+ {
+ LocalDate actual = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[\"2000-01-01\"]");
+ assertEquals(LocalDate.of(2000, 1, 1), actual);
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled()
+ {
+ LocalDate value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ /*
+ /**********************************************************
+ /* Custom format
+ /**********************************************************
+ */
+
+ // for [datatype-jsr310#37]
+ @Test
+ public void testCustomFormat()
+ {
+ Wrapper w = MAPPER.readValue("{\"value\":\"2015_07_28T13:53+0300\"}", Wrapper.class);
+ LocalDate date = w.value;
+ assertEquals(28, date.getDayOfMonth());
+ }
+
+ /*
+ /**********************************************************
+ /* Strict Custom format
+ /**********************************************************
+ */
+
+ // for [modules-java8#148]
+ @Test
+ public void testStrictWithCustomFormat()
+ {
+ try {
+ /*StrictWrapperWithFormat w =*/ MAPPER.readValue(
+ "{\"value\":\"2019-11-31\"}",
+ StrictWrapperWithFormat.class);
+ fail("Should not pass");
+ } catch (InvalidFormatException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String");
+ verifyException(e, "\"2019-11-31\"");
+ }
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidFormat() throws Exception
+ {
+ try {
+ /*StrictWrapperWithFormat w = */ MAPPER.readValue(
+ "{\"value\":\"2019-11-30\"}",
+ StrictWrapperWithFormat.class);
+ fail("Should not pass");
+ } catch (InvalidFormatException e) {
+ // 25-Mar-2021, tatu: Really bad exception message we got... but
+ // it is what it is
+ verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String");
+ verifyException(e, "\"2019-11-30\"");
+ }
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidFormatWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class, () -> {
+ /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30\"}", StrictWrapperWithYearOfEra.class);
+ });
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class, () -> {
+ /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 AD\"}", StrictWrapperWithYearOfEra.class);
+ });
+ }
+
+ @Test
+ public void testStrictCustomFormatForValidDateWithEra() throws Exception
+ {
+ StrictWrapperWithYearOfEra w = MAPPER.readValue("{\"value\":\"2019-11-30 AD\"}", StrictWrapperWithYearOfEra.class);
+
+ assertEquals(w.value, LocalDate.of(2019, 11, 30));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidFormatWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class, () -> {
+ /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 AD\"}", StrictWrapperWithYearWithoutEra.class);
+ });
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class, () -> {
+ /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31\"}", StrictWrapperWithYearWithoutEra.class);
+ });
+ }
+
+ @Test
+ public void testStrictCustomFormatForValidDateWithoutEra() throws Exception
+ {
+ StrictWrapperWithYearWithoutEra w = MAPPER.readValue("{\"value\":\"2019-11-30\"}", StrictWrapperWithYearWithoutEra.class);
+
+ assertEquals(w.value, LocalDate.of(2019, 11, 30));
+ }
+
+ /*
+ /**********************************************************************
+ /* Case-insensitive tests
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializationCaseInsensitiveEnabledOnValue()
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class, o -> o.setFormat(JsonFormat.Value
+ .forPattern("dd-MMM-yyyy")
+ .withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES))
+ )
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDate.class);
+ String[] jsons = new String[] { q("01-Jan-2000"), q("01-JAN-2000"),
+ q("01-jan-2000")};
+ for (String json : jsons) {
+ expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json);
+ }
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveEnabled()
+ {
+ final ObjectMapper mapper = mapperBuilder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES)
+ .withConfigOverride(LocalDate.class, o -> o.setFormat(
+ JsonFormat.Value.forPattern("dd-MMM-yyyy")))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDate.class);
+ String[] jsons = new String[] { q("01-Jan-2000"), q("01-JAN-2000"),
+ q("01-jan-2000")};
+ for(String json : jsons) {
+ expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), json);
+ }
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveDisabled()
+ {
+ final ObjectMapper mapper = mapperBuilder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false)
+ .withConfigOverride(LocalDate.class, o -> o.setFormat(
+ JsonFormat.Value.forPattern("dd-MMM-yyyy")))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDate.class);
+ expectSuccess(reader, LocalDate.of(2000, Month.JANUARY, 1), q("01-Jan-2000"));
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveDisabled_InvalidDate()
+ {
+ final ObjectMapper mapper = mapperBuilder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false)
+ .withConfigOverride(LocalDate.class, o -> JsonFormat.Value.forPattern("dd-MMM-yyyy"))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDate.class);
+ String[] jsons = new String[] { q("01-JAN-2000"), q("01-jan-2000")};
+ for(String json : jsons) {
+ try {
+ reader.readValue(a2q(json));
+ fail("expected DateTimeParseException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.LocalDate` from String ");
+ }
+ }
+ }
+
+ /*
+ /**********************************************************************
+ /* Tests for issue 58 - NUMBER_INT should be specified when deserializing
+ /* LocalDate as EpochDays
+ /**********************************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromNumberInt() {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.NUMBER_INT)))
+ .build();
+
+ assertEquals(LocalDate.of(1970, Month.MAY, 4),
+ mapper.readValue("123", LocalDate.class));
+ }
+
+ @Test
+ public void testStrictDeserializeFromNumberInt()
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ ShapeWrapper w = mapper.readValue("{\"date\":123}", ShapeWrapper.class);
+ LocalDate localDate = w.date;
+
+ assertEquals(LocalDate.of(1970, Month.MAY, 4), localDate);
+ }
+
+ @Test
+ public void testStrictDeserializeFromString()
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+ try {
+ mapper.readValue("{\"value\":123}", Wrapper.class);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize instance of `java.time.LocalDate`");
+ }
+ }
+
+ /**********************************************************************
+ *
+ * coercion config test
+ *
+ /**********************************************************************
+ */
+
+ @Test
+ public void testDeserializeFromIntegerWithCoercionActionFail() {
+ ObjectMapper mapper = newMapperBuilder()
+ .withCoercionConfig(LocalDate.class, cfg ->
+ cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ ).build();
+ MismatchedInputException exception = assertThrows(MismatchedInputException.class,
+ () -> mapper.readValue("123", LocalDate.class));
+
+ assertTrue(exception.getMessage().contains("Cannot coerce Integer value (123) to `java.time.LocalDate`"));
+ }
+
+ @Test
+ public void testDeserializeFromEmptyStringWithCoercionActionFail() {
+ ObjectMapper mapper = newMapperBuilder()
+ .withCoercionConfig(LocalDate.class, cfg ->
+ cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail)
+ ).build();
+
+ MismatchedInputException exception = assertThrows(MismatchedInputException.class,
+ () -> mapper.readValue(a2q("{'value':''}"), Wrapper.class));
+
+ assertTrue(exception.getMessage().contains("Cannot coerce empty String (\"\") to `java.time.LocalDate`"));
+ }
+
+ /*
+ /**********************************************************************
+ /* Helper methods
+ /**********************************************************************
+ */
+
+ private void expectSuccess(ObjectReader reader, Object exp, String json) {
+ final LocalDate value = reader.readValue(a2q(json));
+ assertNotNull(value);
+ assertEquals(exp, value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java
new file mode 100644
index 0000000000..ff1bd6659d
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalDateTimeDeserTest.java
@@ -0,0 +1,737 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.temporal.Temporal;
+import java.util.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+import com.fasterxml.jackson.annotation.OptBoolean;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.deser.DeserializationProblemHandler;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateTimeDeserTest
+ extends DateTimeTestBase
+{
+ private final static ObjectMapper MAPPER = newMapper();
+ private final static ObjectReader READER = MAPPER.readerFor(LocalDateTime.class);
+
+ private final static ObjectMapper STRICT_MAPPER = mapperBuilder()
+ .withConfigOverride(LocalDateTime.class,
+ c -> c.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ private final ObjectReader READER_USING_TIME_ZONE = JsonMapper.builder()
+ .enable(DateTimeFeature.USE_TIME_ZONE_FOR_LENIENT_DATE_PARSING)
+ .build()
+ .readerFor(LocalDateTime.class);
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ final static class StrictWrapper {
+ @JsonFormat(pattern="yyyy-MM-dd HH:mm",
+ lenient = OptBoolean.FALSE)
+ public LocalDateTime value;
+
+ public StrictWrapper() { }
+ public StrictWrapper(LocalDateTime v) { value = v; }
+ }
+
+ final static class StrictWrapperWithYearOfEra {
+ @JsonFormat(pattern="yyyy-MM-dd HH:mm G",
+ lenient = OptBoolean.FALSE)
+ public LocalDateTime value;
+
+ public StrictWrapperWithYearOfEra() { }
+ public StrictWrapperWithYearOfEra(LocalDateTime v) { value = v; }
+ }
+
+ final static class StrictWrapperWithYearWithoutEra {
+ @JsonFormat(pattern="uuuu-MM-dd HH:mm",
+ lenient = OptBoolean.FALSE)
+ public LocalDateTime value;
+
+ public StrictWrapperWithYearWithoutEra() { }
+ public StrictWrapperWithYearWithoutEra(LocalDateTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public LocalDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(LocalDateTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public LocalDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(LocalDateTime v) { value = v; }
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for deserializing from int array
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsTimestamp01()
+ {
+ LocalDateTime value = READER.readValue("[1986,1,17,15,43]");
+ LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp02()
+ {
+ LocalDateTime value = READER.readValue("[2013,8,21,9,22,57]");
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Nanoseconds()
+ {
+ ObjectReader r = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ LocalDateTime value = r.readValue("[2013,8,21,9,22,0,57]");
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Milliseconds()
+ {
+ ObjectReader r = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ LocalDateTime value = r.readValue("[2013,8,21,9,22,0,57]");
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57000000);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Nanoseconds()
+ {
+ ObjectReader r = MAPPER.readerFor(LocalDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829837]");
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds01()
+ {
+ ObjectReader r = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829837]");
+
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds02()
+ {
+ ObjectReader r = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ LocalDateTime value = r.readValue("[2005,11,5,22,31,5,829]");
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829000000);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Nanoseconds() throws Exception
+ {
+ ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ WrapperWithReadTimestampsAsNanosEnabled actual =
+ r.readValue(a2q("{'value':[2013,8,21,9,22,0,57]}"));
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57);
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds01() throws Exception
+ {
+ ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ WrapperWithReadTimestampsAsNanosDisabled actual =
+ r.readValue(a2q("{'value':[2013,8,21,9,22,0,57]}"));
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57000000);
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds02() throws Exception
+ {
+ ObjectReader r = MAPPER.readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ WrapperWithReadTimestampsAsNanosDisabled actual =
+ r.readValue(a2q("{'value':[2013,8,21,9,22,0,4257]}"));
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 4257);
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for deserializing from textual representation
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsString01()
+ {
+ LocalDateTime exp = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43);
+ LocalDateTime value = READER.readValue(q(exp.toString()));
+ assertEquals(exp, value, "The value is not correct.");
+
+ assertEquals(LocalDateTime.of(2000, Month.JANUARY, 1, 12, 0),
+ READER.readValue(q("2000-01-01T12:00")),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02()
+ {
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57);
+ LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString03()
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class);
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for deserializing from textual representation,
+ /* fail cases, leniency checking
+ /**********************************************************
+ */
+
+ // [modules-java#94]: "Z" offset MAY be allowed, requires leniency
+ @Test
+ public void testAllowZuluIfLenient()
+ {
+ final LocalDateTime EXP = LocalDateTime.of(2020, Month.OCTOBER, 22, 4, 16, 20, 504000000);
+ final String input = q("2020-10-22T04:16:20.504Z");
+ final ObjectReader r = MAPPER.readerFor(LocalDateTime.class);
+
+ // First, defaults:
+ assertEquals(EXP, r.readValue(input), "The value is not correct.");
+
+ // but ensure that global timezone setting doesn't matter
+ LocalDateTime value = r.with(TimeZone.getTimeZone(Z_CHICAGO))
+ .readValue(input);
+ assertEquals(EXP, value, "The value is not correct.");
+
+ value = r.with(TimeZone.getTimeZone(Z_BUDAPEST))
+ .readValue(input);
+ assertEquals(EXP, value, "The value is not correct.");
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "UTC, 2020-10-22T04:16:20.504Z, 2020-10-22T04:16:20.504",
+ "Europe/Budapest, 2020-10-22T04:16:20.504Z, 2020-10-22T06:16:20.504",
+ "Europe/Budapest, 2020-10-25T00:16:20.504Z, 2020-10-25T02:16:20.504",
+ "Europe/Budapest, 2020-10-25T01:16:20.504Z, 2020-10-25T02:16:20.504",
+ "America/Chicago, 2020-10-22T04:16:20.504Z, 2020-10-21T23:16:20.504",
+ "America/Chicago, 2020-11-01T06:16:20.504Z, 2020-11-01T01:16:20.504",
+ "America/Chicago, 2020-11-01T07:16:20.504Z, 2020-11-01T01:16:20.504"
+ })
+ public void testUseTimeZoneForZuluIfEnabled(TimeZone zone, String string, LocalDateTime expected) throws Exception
+ {
+ ObjectReader reader = READER_USING_TIME_ZONE.with(zone);
+ LocalDateTime value = reader.readValue(q(string));
+ assertEquals(expected, value);
+ }
+
+ // [modules-java#94]: "Z" offset not allowed if strict mode
+ @Test
+ public void testFailOnZuluIfStrict()
+ {
+ try {
+ STRICT_MAPPER.readValue(q("2020-10-22T00:16:20.504Z"), LocalDateTime.class);
+ fail("Should not pass");
+ } catch (InvalidFormatException e) {
+ verifyException(e, "Cannot deserialize value of type ");
+ verifyException(e, "Should not contain offset when 'strict' mode");
+ }
+ }
+
+ @Test
+ public void testBadDeserializationAsString01()
+ {
+ try {
+ READER.readValue(q("notalocaldatetime"));
+ fail("expected fail");
+ } catch (InvalidFormatException e) {
+ verifyException(e, "Cannot deserialize value of type");
+ verifyException(e, "from String \"");
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString()
+ {
+ String key = "datetime";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsNullStr = null;
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ LocalDateTime actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ LocalDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(actualDateFromNullStr, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "datetime";
+ final ObjectReader objectReader = STRICT_MAPPER.readerFor(MAP_TYPE_REF);
+ final String dateValAsNullStr = null;
+
+ // even with strict, null value should be deserialized without throwing an exception
+ String valueFromNullStr = STRICT_MAPPER.writeValueAsString(asMap(key, dateValAsNullStr));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String dateValAsEmptyStr = "";
+ // TODO: nothing stops us from writing an empty string, maybe there should be a check there too?
+ String valueFromEmptyStr = STRICT_MAPPER.writeValueAsString(asMap("date", dateValAsEmptyStr));
+ // with strict, deserializing an empty string is not permitted
+ try {
+ objectReader.readValue(valueFromEmptyStr);
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize instance of `java.time.LocalDateTime` out of ");
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for alternate array handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[\"2000-01-01T12:00\"]");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Unexpected token (VALUE_STRING) within Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ // works even without the feature enabled
+ assertNull(READER.readValue("[]"));
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ LocalDateTime value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[\"2000-01-01T12:00\"]");
+ assertEquals(LocalDateTime.of(2000, 1, 1, 12, 0, 0, 0),
+ value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ LocalDateTime value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for polymorphic handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ final ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,829837]]");
+ assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422000000);
+
+ final ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,422]]");
+ assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ final ObjectMapper m = newMapperBuilder().
+ addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + LocalDateTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class
+ );
+ assertTrue(value instanceof LocalDateTime, "The value should be a LocalDateTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for `DeserialiazationProblemHandler` usage
+ /**********************************************************
+ */
+
+ @Test
+ public void testDateTimeExceptionIsHandled() throws Throwable
+ {
+ LocalDateTime now = LocalDateTime.now();
+ DeserializationProblemHandler handler = new DeserializationProblemHandler() {
+ @Override
+ public Object handleWeirdStringValue(DeserializationContext ctxt, Class> targetType,
+ String valueToConvert, String failureMsg) {
+ if (LocalDateTime.class == targetType) {
+ if ("now".equals(valueToConvert)) {
+ return now;
+ }
+ }
+ return NOT_HANDLED;
+ }
+ };
+ ObjectMapper handledMapper = mapperBuilder().addHandler(handler).build();
+ assertEquals(now, handledMapper.readValue(q("now"), LocalDateTime.class));
+ }
+
+ @Test
+ public void testUnexpectedTokenIsHandled() throws Throwable
+ {
+ LocalDateTime now = LocalDateTime.now();
+ DeserializationProblemHandler handler = new DeserializationProblemHandler() {
+ @Override
+ public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType,
+ JsonToken t, JsonParser p, String failureMsg) {
+ if (targetType.hasRawClass(LocalDateTime.class)) {
+ if (t.isBoolean()) {
+ return now;
+ }
+ }
+ return NOT_HANDLED;
+ }
+ };
+ ObjectMapper handledMapper = mapperBuilder().addHandler(handler).build();
+ assertEquals(now, handledMapper.readValue("true", LocalDateTime.class));
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for specific reported issues
+ /**********************************************************
+ */
+
+ // [datatype-jrs310#54]
+ @Test
+ public void testDeserializeToDate() throws Exception
+ {
+ ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ String localDateTimeJson = m.writeValueAsString(LocalDateTime.of(1999,10,12,13,45,5));
+ assertEquals("\"1999-10-12T13:45:05\"", localDateTimeJson);
+ Date date = m.readValue(localDateTimeJson,Date.class);
+ assertNotNull(date);
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ cal.setTimeInMillis(date.getTime());
+ assertEquals(1999, cal.get(Calendar.YEAR));
+ assertEquals(12, cal.get(Calendar.DAY_OF_MONTH));
+ assertEquals(13, cal.get(Calendar.HOUR_OF_DAY));
+ assertEquals(45, cal.get(Calendar.MINUTE));
+ assertEquals(5, cal.get(Calendar.SECOND));
+ }
+
+ // [modules-java8#47]: should indicate why timestamp won't work
+ @Test
+ public void testDeserilizeFromSimpleTimestamp() throws Exception
+ {
+ ObjectReader r = MAPPER.readerFor(LocalDateTime.class);
+ LocalDateTime value;
+ try {
+ value = r.readValue("1235");
+ fail("Should not succeed, instead got: "+value);
+ } catch (MismatchedInputException e) {
+ verifyException(e, "raw timestamp (1235) not allowed for `java.time.LocalDateTime`");
+ }
+ }
+
+ /*
+ /**********************************************************************
+ /* Case-insensitive tests
+ /**********************************************************************
+ */
+
+ // [modules-java8#80]: handle case-insensitive date/time
+ @Test
+ public void testDeserializationCaseInsensitiveEnabledOnValue() throws Throwable
+ {
+ final ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDateTime.class, o -> o.setFormat(JsonFormat.Value
+ .forPattern("dd-MMM-yyyy HH:mm")
+ .withFeature(Feature.ACCEPT_CASE_INSENSITIVE_VALUES)))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDateTime.class);
+ String[] jsons = new String[] {"'01-Jan-2000 13:14'","'01-JAN-2000 13:14'", "'01-jan-2000 13:14'"};
+ for(String json : jsons) {
+ expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 14), json);
+ }
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveEnabled() throws Throwable
+ {
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, true)
+ .withConfigOverride(LocalDateTime.class, o -> o.setFormat(
+ JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm")))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDateTime.class);
+ String[] jsons = new String[] {"'01-Jan-2000 13:45'","'01-JAN-2000 13:45'", "'01-jan-2000 13:45'"};
+ for(String json : jsons) {
+ expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45), json);
+ }
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveDisabled() throws Throwable
+ {
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false)
+ .withConfigOverride(LocalDateTime.class, o -> o.setFormat(
+ JsonFormat.Value.forPattern("dd-MMM-yyyy HH:mm")))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDateTime.class);
+ expectSuccess(reader, LocalDateTime.of(2000, Month.JANUARY, 1, 13, 45),
+ q("01-Jan-2000 13:45"));
+ }
+
+ @Test
+ public void testDeserializationCaseInsensitiveDisabled_InvalidDate() throws Throwable
+ {
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES, false)
+ .withConfigOverride(LocalDateTime.class, o -> o.setFormat(
+ JsonFormat.Value.forPattern("dd-MMM-yyyy")))
+ .build();
+ ObjectReader reader = mapper.readerFor(LocalDateTime.class);
+ String[] jsons = new String[] {"'01-JAN-2000'", "'01-jan-2000'"};
+ for(String json : jsons) {
+ try {
+ reader.readValue(a2q(json));
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Failed to deserialize `java.time.LocalDateTime` (with format");
+ }
+ }
+ }
+
+ /*
+ /**********************************************************************
+ /* Strict JsonFormat tests
+ /**********************************************************************
+ */
+
+ // [modules-java8#148]: handle strict deserializaiton for date/time
+ @Test
+ public void testStrictCustomFormatForInvalidFormat() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapper w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45\"}", StrictWrapper.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidFormatWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45\"}", StrictWrapperWithYearOfEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 15:45 AD\"}", StrictWrapperWithYearOfEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidTimeWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 25:45 AD\"}", StrictWrapperWithYearOfEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateAndTimeWithEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearOfEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 25:45 AD\"}", StrictWrapperWithYearOfEra.class));
+
+ }
+
+ @Test
+ public void testStrictCustomFormatValidDateAndTimeWithEra() throws Exception
+ {
+ StrictWrapperWithYearOfEra w = MAPPER.readValue("{\"value\":\"2019-11-30 20:45 AD\"}", StrictWrapperWithYearOfEra.class);
+
+ assertEquals(w.value, LocalDateTime.of(2019, 11, 30, 20, 45));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidFormatWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 15:45 AD\"}", StrictWrapperWithYearWithoutEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidTimeWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-30 25:45\"}", StrictWrapperWithYearWithoutEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 15:45\"}", StrictWrapperWithYearWithoutEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForInvalidDateAndTimeWithoutEra() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapperWithYearWithoutEra w =*/ MAPPER.readValue("{\"value\":\"2019-11-31 25:45\"}", StrictWrapperWithYearWithoutEra.class));
+ }
+
+ @Test
+ public void testStrictCustomFormatForValidDateAndTimeWithoutEra() throws Exception
+ {
+ StrictWrapperWithYearWithoutEra w = MAPPER.readValue("{\"value\":\"2019-11-30 20:45\"}",
+ StrictWrapperWithYearWithoutEra.class);
+
+ assertEquals(w.value, LocalDateTime.of(2019, 11, 30, 20, 45));
+ }
+
+ // [datatype-jsr310#124] Issue serializing and deserializing LocalDateTime.MAX and LocalDateTime.MIN
+ @Test
+ public void testDeserializationOfLocalDateTimeMax() throws Exception
+ {
+ ObjectMapper enabledMapper = mapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
+ _testLocalDateTimeRoundTrip(enabledMapper, LocalDateTime.MAX);
+ _testLocalDateTimeRoundTrip(enabledMapper, LocalDateTime.MIN);
+
+ ObjectMapper disabledMapper = mapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
+ _testLocalDateTimeRoundTrip(disabledMapper, LocalDateTime.MAX);
+ _testLocalDateTimeRoundTrip(disabledMapper, LocalDateTime.MIN);
+ }
+
+ private void _testLocalDateTimeRoundTrip(ObjectMapper mapper, LocalDateTime localDateTime)
+ throws Exception
+ {
+ String ser = mapper.writeValueAsString(localDateTime);
+ LocalDateTime result = mapper.readValue(ser, LocalDateTime.class);
+ assertEquals(localDateTime, result);
+ }
+
+ private void expectSuccess(ObjectReader reader, Object exp, String json) throws IOException {
+ final LocalDateTime value = reader.readValue(a2q(json));
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(exp, value, "The value is not correct.");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java
new file mode 100644
index 0000000000..da8c801086
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/LocalTimeDeserTest.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.LocalTime;
+import java.time.temporal.Temporal;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+import com.fasterxml.jackson.annotation.OptBoolean;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalTimeDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(LocalTime.class);
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ final static class StrictWrapper {
+ @JsonFormat(pattern="HH:mm", lenient = OptBoolean.FALSE)
+ public LocalTime value;
+
+ public StrictWrapper() { }
+ public StrictWrapper(LocalTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public LocalTime value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(LocalTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public LocalTime value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(LocalTime v) { value = v; }
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp01() throws Exception
+ {
+ LocalTime time = LocalTime.of(15, 43);
+ LocalTime value = READER.readValue("[15,43]");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp02() throws Exception
+ {
+ LocalTime time = LocalTime.of(9, 22, 57);
+ LocalTime value = READER.readValue("[9,22,57]");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ LocalTime value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[9,22,0,57]");
+ assertEquals(LocalTime.of(9, 22, 0, 57), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Milliseconds() throws Exception
+ {
+ LocalTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[9,22,0,57]");
+ assertEquals(LocalTime.of(9, 22, 0, 57000000), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Nanoseconds() throws Exception
+ {
+ LocalTime value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829837]");
+ assertEquals(LocalTime.of(22, 31, 5, 829837), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds01() throws Exception
+ {
+ LocalTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829837]");
+ assertEquals(LocalTime.of(22, 31, 5, 829837), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds02() throws Exception
+ {
+ LocalTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829]");
+ assertEquals(LocalTime.of(22, 31, 5, 829000000), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Nanoseconds() throws Exception
+ {
+ ObjectReader wrapperReader =
+ newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ WrapperWithReadTimestampsAsNanosEnabled actual = wrapperReader
+ .readValue(a2q("{'value':[9,22,0,57]}"));
+ assertEquals(LocalTime.of(9, 22, 0, 57), actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds01() throws Exception
+ {
+ ObjectReader wrapperReader =
+ newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ WrapperWithReadTimestampsAsNanosDisabled actual = wrapperReader
+ .readValue(a2q("{'value':[9,22,0,57]}"));
+ assertEquals(LocalTime.of(9, 22, 0, 57000000), actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds02() throws Exception
+ {
+ ObjectReader wrapperReader =
+ newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ WrapperWithReadTimestampsAsNanosDisabled actual = wrapperReader
+ .readValue(a2q("{'value':[9,22,0,4257]}"));
+ assertEquals(LocalTime.of(9, 22, 0, 4257), actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationFromString() throws Exception
+ {
+ LocalTime time = LocalTime.of(15, 43);
+ LocalTime value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+
+ expectSuccess(LocalTime.of(12, 0), "'12:00'");
+
+ time = LocalTime.of(9, 22, 57);
+ value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+
+ time = LocalTime.of(22, 31, 5, 829837);
+ value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testBadDeserializationFromString() throws Throwable
+ {
+ try {
+ READER.readValue(q("notalocaltime"));
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.LocalTime` from String");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue(a2q("['12:00']"));
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Unexpected token (VALUE_STRING) within Array");
+ }
+
+ // 25-Jul-2017, tatu: Why does it work? Is it supposed to?
+ // works even without the feature enabled
+ assertNull(READER.readValue("[]"));
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ LocalTime value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue(a2q("['12:00']"));
+ expect(LocalTime.of(12, 0), value);
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ LocalTime value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue(a2q("[]"));
+ assertNull(value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readerFor(Temporal.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[\"" + LocalTime.class.getName() + "\",[22,31,5,829837]]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertTrue(value instanceof LocalTime, "The value should be a LocalTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 422000000);
+
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readerFor(Temporal.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[\"" + LocalTime.class.getName() + "\",[22,31,5,422]]");
+ assertTrue(value instanceof LocalTime, "The value should be a LocalTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readValue(
+ "[\"" + LocalTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class
+ );
+ assertTrue(value instanceof LocalTime, "The value should be a LocalTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "localTime";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ LocalTime actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ LocalTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "localTime";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(LocalTime.class,
+ c -> c.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", ""));
+ assertThrows(MismatchedInputException.class,
+ () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ /*
+ /**********************************************************************
+ /* Strict JsonFormat tests
+ /**********************************************************************
+ */
+
+ // [modules-java8#148]: handle strict deserializaiton for date/time
+
+ @Test
+ public void testStrictCustomFormatInvalidTime() throws Exception
+ {
+ assertThrows(InvalidFormatException.class,
+ () -> /*StrictWrapper w =*/ MAPPER.readValue("{\"value\":\"25:45\"}", StrictWrapper.class));
+ }
+
+ private void expectSuccess(Object exp, String aposJson) throws Exception {
+ final LocalTime value = READER.readValue(a2q(aposJson));
+ expect(exp, value);
+ }
+
+ private static void expect(Object exp, Object value) {
+ assertEquals(exp, value, "The value is not correct.");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java
new file mode 100644
index 0000000000..b70a03c448
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/MonthDayDeserTest.java
@@ -0,0 +1,211 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Month;
+import java.time.MonthDay;
+import java.time.temporal.TemporalAccessor;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class MonthDayDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(MonthDay.class);
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ static class Wrapper {
+ @JsonFormat(pattern="MM/dd")
+ public MonthDay value;
+
+ public Wrapper(MonthDay v) { value = v; }
+ public Wrapper() { }
+ }
+
+ static class WrapperAsArray {
+ @JsonFormat(shape = JsonFormat.Shape.ARRAY)
+ public MonthDay value;
+
+ public WrapperAsArray(MonthDay v) { value = v; }
+ public WrapperAsArray() { }
+ }
+
+
+ @Test
+ public void testDeserializationAsString01() throws Exception
+ {
+ expectSuccess(MonthDay.of(Month.JANUARY, 1), "'--01-01'");
+ }
+
+ @Test
+ public void testBadDeserializationAsString01() throws Throwable
+ {
+ try {
+ READER.readValue(q("notamonthday"));
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.MonthDay` from String");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ read("['--01-01']");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ // expecting array-of-ints
+ verifyException(e, "Unexpected token");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ // since 2.10, empty array taken as `null`
+
+ MonthDay value = READER.readValue("[]");
+ assertNull(value);
+
+ value = newMapper()
+ .readerFor(MonthDay.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ MonthDay value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue(a2q("['--01-01']"));
+ expect(MonthDay.of(Month.JANUARY, 1), value);
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ MonthDay value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ @Test
+ public void testDeserialization01() throws Exception
+ {
+ assertEquals(MonthDay.of(Month.JANUARY, 17), MAPPER.readValue("\"--01-17\"", MonthDay.class),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserialization02() throws Exception
+ {
+ assertEquals(MonthDay.of(Month.AUGUST, 21),
+ MAPPER.readValue("\"--08-21\"", MonthDay.class),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ final ObjectMapper mapper = mapperBuilder()
+ .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
+ .build();
+ MonthDay monthDay = MonthDay.of(Month.NOVEMBER, 5);
+ TemporalAccessor value = mapper.readValue("[\"" + MonthDay.class.getName() + "\",\"--11-05\"]", TemporalAccessor.class);
+ assertEquals(monthDay, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testFormatAnnotation() throws Exception
+ {
+ final Wrapper input = new Wrapper(MonthDay.of(12, 28));
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals("{\"value\":\"12/28\"}", json);
+
+ Wrapper output = MAPPER.readValue(json, Wrapper.class);
+ assertEquals(input.value, output.value);
+ }
+
+ @Test
+ public void testFormatAnnotationArray() throws Exception
+ {
+ final WrapperAsArray input = new WrapperAsArray(MonthDay.of(12, 28));
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals("{\"value\":[12,28]}", json);
+
+ // 13-May-2019, tatu: [modules-java#107] not fully implemented so can't yet test
+ WrapperAsArray output = MAPPER.readValue(json, WrapperAsArray.class);
+ assertEquals(input.value, output.value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ // minor changes in 2.12
+ @Test
+ public void testDeserializeFromEmptyString() throws Exception
+ {
+ final String key = "monthDay";
+
+ // First: by default, lenient, so empty String fine
+ final ObjectReader objectReader = MAPPER.readerFor(MAP_TYPE_REF);
+
+ String doc = MAPPER.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(doc);
+ assertNull(actualMapFromNullStr.get(key));
+
+ doc = MAPPER.writeValueAsString(asMap(key, ""));
+ assertNotNull(objectReader.readValue(doc));
+
+ // But can make strict:
+ final ObjectMapper strictMapper = mapperBuilder()
+ .withConfigOverride(MonthDay.class, o -> o.setFormat(
+ JsonFormat.Value.forLeniency(false)))
+ .build();
+ doc = strictMapper.writeValueAsString(asMap("date", ""));
+ try {
+ strictMapper.readerFor(MAP_TYPE_REF)
+ .readValue(doc);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "not allowed because 'strict' mode set for");
+ }
+ }
+
+ private void expectSuccess(Object exp, String aposJson) throws Exception {
+ final MonthDay value = read(aposJson);
+ notNull(value);
+ expect(exp, value);
+ }
+
+ private MonthDay read(final String aposJson) throws Exception {
+ return READER.readValue(a2q(aposJson));
+ }
+
+ private static void notNull(Object value) {
+ assertNotNull(value, "The value should not be null.");
+ }
+
+ private static void expect(Object exp, Object value) {
+ assertEquals(exp, value, "The value is not correct.");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java
new file mode 100644
index 0000000000..71362dd509
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetDateTimeDeserTest.java
@@ -0,0 +1,824 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OffsetDateTimeDeserTest
+ extends DateTimeTestBase
+{
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ private static final ZoneId Z1 = ZoneId.of("America/Chicago");
+
+ private static final ZoneId Z2 = ZoneId.of("America/Anchorage");
+
+ private static final ZoneId Z3 = ZoneId.of("America/Los_Angeles");
+
+ final static class Wrapper {
+ @JsonFormat(
+ pattern="yyyy_MM_dd'T'HH:mm:ssZ",
+ shape=JsonFormat.Shape.STRING)
+ public OffsetDateTime value;
+
+ public Wrapper() { }
+ public Wrapper(OffsetDateTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public OffsetDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(OffsetDateTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public OffsetDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(OffsetDateTime v) { value = v; }
+ }
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectMapper MAPPER_DEFAULT_TZ = newMapper(TimeZone.getDefault());
+
+ @Test
+ public void testDeserializationAsFloat01WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER.readValue("0.000000000", OffsetDateTime.class);
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat01WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readValue("0.000000000", OffsetDateTime.class);
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat02WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ OffsetDateTime value = MAPPER.readValue("123456789.183917322", OffsetDateTime.class);
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat02WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(TimeZone.getDefault())
+ .readValue("123456789.183917322");
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat03WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+
+ OffsetDateTime value = MAPPER
+ .readerFor(OffsetDateTime.class)
+ .readValue(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()));
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsFloat03WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .readValue(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()));
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt01NanosecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("0");
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt01NanosecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .with(TimeZone.getDefault())
+ .readValue("0");
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt01MillisecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("0");
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt01MillisecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("0");
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02NanosecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("123456789");
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02NanosecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("123456789");
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02MillisecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("123456789422");
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02MillisecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("123456789422");
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt03NanosecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.toEpochSecond()));
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt03NanosecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ date = date.minus(date.getNano(), ChronoUnit.NANOS);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.toEpochSecond()));
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt03MillisecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.toInstant().toEpochMilli()));
+
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt03MillisecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ date = date.minus(date.getNano() - (date.get(ChronoField.MILLI_OF_SECOND) * 1_000_000L), ChronoUnit.NANOS);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(Long.toString(date.toInstant().toEpochMilli()));
+
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt04NanosecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z1);
+ ObjectMapper m = newMapper();
+ WrapperWithReadTimestampsAsNanosEnabled actual = m.readValue(
+ a2q("{'value':123456789}"),
+ WrapperWithReadTimestampsAsNanosEnabled.class);
+
+ assertNotNull(actual, "The actual should not be null.");
+ assertNotNull(actual.value, "The actual value should not be null.");
+ assertIsEqual(date, actual.value);
+ assertEquals(ZoneOffset.UTC, actual.value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt04NanosecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z1);
+ ObjectMapper m = mapperBuilder()
+ .defaultTimeZone(TimeZone.getDefault())
+ .build();
+ WrapperWithReadTimestampsAsNanosEnabled actual = m.readValue(
+ a2q("{'value':123456789}"),
+ WrapperWithReadTimestampsAsNanosEnabled.class);
+
+ assertNotNull(actual, "The actual should not be null.");
+ assertNotNull(actual.value, "The actual value should not be null.");
+ assertIsEqual(date, actual.value);
+ assertEquals(getDefaultOffset(date), actual.value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt04MillisecondsWithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z1);
+ ObjectMapper m = newMapper();
+ WrapperWithReadTimestampsAsNanosDisabled actual = m.readValue(
+ a2q("{'value':123456789422}"),
+ WrapperWithReadTimestampsAsNanosDisabled.class);
+
+ assertNotNull(actual, "The actual should not be null.");
+ assertNotNull(actual.value, "The actual value should not be null.");
+ assertIsEqual(date, actual.value);
+ assertEquals(ZoneOffset.UTC, actual.value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt04MillisecondsWithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z1);
+ ObjectMapper m = mapperBuilder()
+ .defaultTimeZone(TimeZone.getDefault())
+ .build();
+ WrapperWithReadTimestampsAsNanosDisabled actual = m.readValue(
+ a2q("{'value':123456789422}"),
+ WrapperWithReadTimestampsAsNanosDisabled.class);
+
+ assertNotNull(actual, "The actual should not be null.");
+ assertNotNull(actual.value, "The actual value should not be null.");
+ assertIsEqual(date, actual.value);
+ assertEquals(getDefaultOffset(date), actual.value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString01WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString01WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString01WithTimeZoneTurnedOff() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z1), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString01WithTimeZoneColonless() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ ObjectReader r = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+
+ String sDate = offsetWithoutColon(FORMATTER.format(date));
+
+ OffsetDateTime value = r.readValue('"' + sDate + '"');
+
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z1), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02WithTimeZoneTurnedOff() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z2), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString02WithTimeZoneColonless() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ ObjectReader r = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+
+ String sDate = offsetWithoutColon(FORMATTER.format(date));
+
+ OffsetDateTime value = r.readValue('"' + sDate + '"');
+
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z2), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString03WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ OffsetDateTime value = MAPPER.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(ZoneOffset.UTC, value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString03WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(getDefaultOffset(date), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsString03WithTimeZoneTurnedOff() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ OffsetDateTime value = MAPPER_DEFAULT_TZ.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue('"' + FORMATTER.format(date) + '"');
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z3), value.getOffset(), "The time zone is not correct.");
+ }
+
+
+ @Test
+ public void testDeserializationAsString03WithTimeZoneColonless() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ ObjectReader r = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+
+ String sDate = offsetWithoutColon(FORMATTER.format(date));
+
+ OffsetDateTime value = r.readValue('"' + sDate + '"');
+
+ assertIsEqual(date, value);
+ assertEquals(getOffset(value, Z3), value.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", Temporal.class
+ );
+
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ ObjectMapper m = newMapperBuilder(TimeZone.getDefault())
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", Temporal.class
+ );
+
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 0), Z2);
+ ObjectMapper m = newMapperBuilder(TimeZone.getDefault())
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789422]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 422000000), Z2);
+ ObjectMapper m = newMapperBuilder(TimeZone.getDefault())
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",123456789422]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo04WithoutTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(ZoneOffset.UTC, ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo04WithTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ ObjectMapper m = newMapperBuilder(TimeZone.getDefault())
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .with(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]");
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ assertIsEqual(date, (OffsetDateTime) value);
+ assertEquals(getDefaultOffset(date), ((OffsetDateTime) value).getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo04WithTimeZoneTurnedOff() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+
+ ObjectMapper m = newMapperBuilder(TimeZone.getDefault())
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readerFor(Temporal.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]");
+
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ OffsetDateTime cast = (OffsetDateTime) value;
+ assertIsEqual(date, cast);
+ assertEquals(getOffset(cast, Z3), cast.getOffset(), "The time zone is not correct.");
+ }
+
+ @Test
+ public void testCustomPatternWithAnnotations() throws Exception
+ {
+ OffsetDateTime inputValue = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), UTC);
+ final Wrapper input = new Wrapper(inputValue);
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals(a2q("{'value':'1970_01_01T00:00:00+0000'}"), json);
+
+ Wrapper result = MAPPER.readValue(json, Wrapper.class);
+ assertEquals(input.value, result.value);
+ }
+
+ // [datatype-jsr310#79]
+ @Test
+ public void testRoundTripOfOffsetDateTimeAndJavaUtilDate() throws Exception
+ {
+ Instant givenInstant = LocalDate.of(2016, 1, 1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant();
+
+ String json = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS,
+ SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(java.util.Date.from(givenInstant));
+ OffsetDateTime actual = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(json);
+ assertEquals(givenInstant.atOffset(ZoneOffset.UTC), actual);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "OffsetDateTime";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ OffsetDateTime actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ OffsetDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "OffsetDateTime";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(OffsetDateTime.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ // [module-java8#166]
+ @Test
+ public void testDeserializationNoAdjustIfMIN() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.MIN;
+ ObjectMapper m = mapperBuilder()
+ .enable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .defaultTimeZone(TimeZone.getTimeZone(Z1))
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class
+ );
+
+ assertTrue(value instanceof OffsetDateTime, "The value should be an OffsetDateTime.");
+ OffsetDateTime actualValue = (OffsetDateTime) value;
+ assertIsEqual(date, actualValue);
+ assertEquals(date.getOffset(),actualValue.getOffset());
+ }
+
+ @Test
+ public void testDeserializationNoAdjustIfMAX() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.MAX;
+ ObjectMapper m = mapperBuilder()
+ .enable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .defaultTimeZone(TimeZone.getTimeZone(Z1))
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = m.readValue(
+ "[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", Temporal.class
+ );
+
+ assertInstanceOf(OffsetDateTime.class, value, "The value should be an OffsetDateTime.");
+ OffsetDateTime actualValue = (OffsetDateTime) value;
+ assertIsEqual(date, actualValue);
+ assertEquals(date.getOffset(),actualValue.getOffset());
+ }
+
+ // [jackson-modules-java8#308] Can't deserialize OffsetDateTime.MIN: Invalid value for EpochDay
+ @Test
+ public void testOffsetDateTimeMinOrMax() throws Exception
+ {
+ _testOffsetDateTimeMinOrMax(OffsetDateTime.MIN);
+ _testOffsetDateTimeMinOrMax(OffsetDateTime.MAX);
+ }
+
+ @Test
+ public void OffsetDateTime_with_offset_can_be_deserialized() throws Exception {
+ ObjectReader r = MAPPER.readerFor(OffsetDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+
+ String base = "2015-07-24T12:23:34.184";
+ for (String offset : Arrays.asList("+00", "-00")) {
+ String time = base + offset;
+ if (!System.getProperty("java.version").startsWith("1.8")) {
+ // JDK 8 cannot parse hour offsets without minutes
+ assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"'));
+ }
+ assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"'));
+ assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"'));
+ assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + "30" + '"'));
+ assertIsEqual(OffsetDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30"), r.readValue('"' + time + ":30" + '"'));
+ }
+
+ for (String prefix : Arrays.asList("-", "+")) {
+ for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) {
+ String time = base + prefix + hours;
+ OffsetDateTime expectedHour = OffsetDateTime.parse(time + ":00");
+ if (!System.getProperty("java.version").startsWith("1.8")) {
+ // JDK 8 cannot parse hour offsets without minutes
+ assertIsEqual(expectedHour, r.readValue('"' + time + '"'));
+ }
+ assertIsEqual(expectedHour, r.readValue('"' + time + "00" + '"'));
+ assertIsEqual(expectedHour, r.readValue('"' + time + ":00" + '"'));
+ assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"'));
+ assertIsEqual(OffsetDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"'));
+ }
+ }
+ }
+
+ private void _testOffsetDateTimeMinOrMax(OffsetDateTime offsetDateTime)
+ throws Exception
+ {
+ String ser = MAPPER.writeValueAsString(offsetDateTime);
+ OffsetDateTime result = MAPPER.readValue(ser, OffsetDateTime.class);
+ assertIsEqual(offsetDateTime, result);
+ }
+
+ private static void assertIsEqual(OffsetDateTime expected, OffsetDateTime actual)
+ {
+ assertTrue(expected.isEqual(actual),
+ "The value is not correct. Expected timezone-adjusted <" + expected + ">, actual <" + actual + ">.");
+ }
+
+ private static ZoneOffset getDefaultOffset(OffsetDateTime date)
+ {
+ return ZoneId.systemDefault().getRules().getOffset(date.toLocalDateTime());
+ }
+
+ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone)
+ {
+ return zone.getRules().getOffset(date.toLocalDateTime());
+ }
+
+ private static String offsetWithoutColon(String string){
+ return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString();
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java
new file mode 100644
index 0000000000..ea8cd44f23
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OffsetTimeDeserTest.java
@@ -0,0 +1,349 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.time.temporal.Temporal;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OffsetTimeDeserTest extends DateTimeTestBase
+{
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ // for [datatype-jsr310#45]
+ static class Pojo45s {
+ public String name;
+ public List objects;
+ }
+
+ static class Pojo45 {
+ public java.time.LocalDate partDate;
+ public java.time.OffsetTime starttime;
+ public java.time.OffsetTime endtime;
+ public String comments;
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public OffsetTime value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(OffsetTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public OffsetTime value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(OffsetTime v) { value = v; }
+ }
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(OffsetTime.class);
+
+ @Test
+ public void testDeserializationAsTimestamp01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300"));
+ OffsetTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[15,43,\"+0300\"]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630"));
+ OffsetTime value = READER.readValue("[9,22,57,\"-06:30\"]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630"));
+ OffsetTime value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[9,22,0,57,\"-06:30\"]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp03Milliseconds() throws Exception {
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57000000, ZoneOffset.of("-0630"));
+ OffsetTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[9,22,0,57,\"-06:30\"]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Nanoseconds() throws Exception {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+ OffsetTime value = READER
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829837,\"+11:00\"]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+ OffsetTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829837,\"+11:00\"]");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp04Milliseconds02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829000000, ZoneOffset.of("+1100"));
+ OffsetTime value = READER
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[22,31,5,829,\"+11:00\"]");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Nanoseconds() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630"));
+ WrapperWithReadTimestampsAsNanosEnabled actual = reader
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(a2q("{'value':[9,22,0,57,'-06:30']}"));
+
+ assertNotNull(actual, "The value should not be null.");
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds01() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57000000, ZoneOffset.of("-0630"));
+ WrapperWithReadTimestampsAsNanosDisabled actual = reader
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(a2q("{'value':[9,22,0,57,'-06:30']}"));
+
+ assertNotNull(actual, "The value should not be null.");
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsTimestamp05Milliseconds02() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ OffsetTime time = OffsetTime.of(9, 22, 0, 4257, ZoneOffset.of("-0630"));
+ WrapperWithReadTimestampsAsNanosDisabled actual = reader
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(a2q("{'value':[9,22,0,4257,'-06:30']}"));
+
+ assertNotNull(actual, "The value should not be null.");
+ assertEquals(time, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationFromString01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300"));
+ OffsetTime value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+
+ time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630"));
+ value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+
+ time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+ value = READER.readValue('"' + time.toString() + '"');
+ assertEquals(time, value, "The value is not correct.");
+
+ assertEquals(OffsetTime.of(12, 0, 0, 0, ZoneOffset.UTC),
+ READER.readValue(a2q("'12:00Z'")));
+ }
+
+ @Test
+ public void testBadDeserializationFromString01() throws Throwable
+ {
+ try {
+ READER.readValue(q("notanoffsettime"));
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.OffsetTime` from String");
+ }
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readerFor(Temporal.class)
+ .with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue(
+ "[\"" + OffsetTime.class.getName() + "\",[22,31,5,829837,\"+11:00\"]]");
+ assertInstanceOf(OffsetTime.class, value, "The value should be a OffsetTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 422000000, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readerFor(Temporal.class)
+ .without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .readValue("[\"" + OffsetTime.class.getName() + "\",[22,31,5,422,\"+11:00\"]]");
+
+ assertNotNull(value, "The value should not be null.");
+ assertInstanceOf(OffsetTime.class, value, "The value should be a OffsetTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readValue(
+ "[\"" + OffsetTime.class.getName() + "\",\"" + time.toString() + "\"]", Temporal.class
+ );
+ assertTrue(value instanceof OffsetTime, "The value should be a OffsetTime.");
+ assertEquals(time, value, "The value is not correct.");
+ }
+
+ // for [datatype-jsr310#45]
+ @Test
+ public void testDeserOfArrayOf() throws Exception
+ {
+ final String JSON = a2q
+ ("{'name':'test','objects':[{'partDate':[2015,10,13],'starttime':[15,7,'+0'],'endtime':[2,14,'+0'],'comments':'in the comments'}]}");
+ Pojo45s result = READER.forType(Pojo45s.class).readValue(JSON);
+ assertNotNull(result);
+ assertNotNull(result.objects);
+ assertEquals(1, result.objects.size());
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue(a2q("['12:00Z']"));
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ // not the greatest error message...
+ verifyException(e, "Unexpected token (VALUE_STRING) within Array, expected");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ // works even without the feature enabled
+ assertNull(READER.readValue(a2q("[]")));
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ OffsetTime value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue(a2q("['12:00Z']"));
+ assertEquals(OffsetTime.of(12, 0, 0, 0, ZoneOffset.UTC), value);
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ OffsetTime value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "OffsetTime";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ OffsetTime actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ OffsetTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "OffsetTime";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(OffsetTime.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java
new file mode 100644
index 0000000000..c9dd95bb2b
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/OneBasedMonthDeserTest.java
@@ -0,0 +1,207 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Month;
+import java.time.temporal.TemporalAccessor;
+
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OneBasedMonthDeserTest extends DateTimeTestBase
+{
+ static class Wrapper {
+ public Month value;
+
+ public Wrapper(Month v) { value = v; }
+ public Wrapper() { }
+ }
+
+ @ParameterizedTest
+ @EnumSource(Month.class)
+ public void testDeserializationAsString01_oneBased(Month expectedMonth) throws Exception
+ {
+ int monthNum = expectedMonth.getValue();
+ assertEquals(expectedMonth, readerForOneBased().readValue("\"" + monthNum + '"'));
+ }
+
+ @ParameterizedTest
+ @EnumSource(Month.class)
+ public void testDeserializationAsString01_zeroBased(Month expectedMonth) throws Exception
+ {
+ int monthNum = expectedMonth.ordinal();
+ assertEquals(expectedMonth, readerForZeroBased().readValue("\"" + monthNum + '"'));
+ }
+
+
+ @ParameterizedTest
+ @EnumSource(Month.class)
+ public void testDeserializationAsString02_oneBased(Month month) throws Exception
+ {
+ assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
+ }
+
+ @ParameterizedTest
+ @EnumSource(Month.class)
+ public void testDeserializationAsString02_zeroBased(Month month) throws Exception
+ {
+ assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "notamonth , 'Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class:'",
+ "JANUAR , 'Cannot deserialize value of type `java.time.Month` from String \"JANUAR\": not one of the values accepted for Enum class:'",
+ "march , 'Cannot deserialize value of type `java.time.Month` from String \"march\": not one of the values accepted for Enum class:'",
+ "0 , 'Month number 0 not allowed for 1-based Month.'",
+ "13 , 'Month number 13 not allowed for 1-based Month.'",
+ })
+ public void testBadDeserializationAsString01_oneBased(String monthSpec, String expectedMessage) {
+ String value = "\"" + monthSpec + '"';
+ assertError(
+ () -> readerForOneBased().readValue(value),
+ InvalidFormatException.class,
+ expectedMessage
+ );
+ }
+
+ static void assertError(Executable codeToRun, Class extends Throwable> expectedException, String expectedMessage) {
+ try {
+ codeToRun.execute();
+ fail(String.format("Expecting %s, but nothing was thrown!", expectedException.getName()));
+ } catch (Throwable actualException) {
+ if (!expectedException.isInstance(actualException)) {
+ fail(String.format("Expecting exception of type %s, but %s was thrown instead", expectedException.getName(), actualException.getClass().getName()));
+ }
+ if (actualException.getMessage() == null || !actualException.getMessage().contains(expectedMessage)) {
+ fail(String.format("Expecting exception with message containing: '%s', but the actual error message was:'%s'", expectedMessage, actualException.getMessage()));
+ }
+ }
+ }
+
+
+ @Test
+ public void testDeserialization01_zeroBased() throws Exception
+ {
+ assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("1"));
+ }
+
+ @Test
+ public void testDeserialization01_oneBased() throws Exception
+ {
+ assertEquals(Month.JANUARY, readerForOneBased().readValue("1"));
+ }
+
+ @Test
+ public void testDeserialization02_zeroBased() throws Exception
+ {
+ assertEquals(Month.SEPTEMBER, readerForZeroBased().readValue("\"8\""));
+ }
+
+ @Test
+ public void testDeserialization02_oneBased() throws Exception
+ {
+ assertEquals(Month.AUGUST, readerForOneBased().readValue("\"8\""));
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01_oneBased() throws Exception
+ {
+ ObjectMapper MAPPER = JsonMapper.builder()
+ .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
+ .enable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build();
+
+ TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",11]", TemporalAccessor.class);
+ assertEquals(Month.NOVEMBER, value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01_zeroBased() throws Exception
+ {
+ ObjectMapper MAPPER = JsonMapper.builder()
+ .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
+ .disable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build();
+
+ TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class);
+ assertEquals(Month.DECEMBER, value);
+ }
+
+ @Test
+ public void testFormatAnnotation_zeroBased() throws Exception
+ {
+ Wrapper output = readerForZeroBased()
+ .forType(Wrapper.class)
+ .readValue("{\"value\":\"11\"}");
+ assertEquals(new Wrapper(Month.DECEMBER).value, output.value);
+ }
+
+ @Test
+ public void testFormatAnnotation_oneBased() throws Exception
+ {
+ Wrapper output = readerForOneBased()
+ .forType(Wrapper.class)
+ .readValue("{\"value\":\"11\"}");
+ assertEquals(new Wrapper(Month.NOVEMBER).value, output.value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializeFromEmptyString() throws Exception
+ {
+ final ObjectMapper mapper = newMapper();
+
+ // Nulls are handled in general way, not by deserializer so they are ok
+ Month m = mapper.readerFor(Month.class).readValue(" null ");
+ assertNull(m);
+
+ // But coercion from empty String not enabled for Enums by default:
+ try {
+ mapper.readerFor(Month.class).readValue("\"\"");
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ }
+ // But can allow coercion of empty String to, say, null
+ ObjectMapper emptyStringMapper = mapperBuilder()
+ .withCoercionConfig(Month.class,
+ h -> h.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull))
+ .build();
+ m = emptyStringMapper.readerFor(Month.class).readValue("\"\"");
+ assertNull(m);
+ }
+
+ private ObjectReader readerForZeroBased() {
+ return JsonMapper.builder()
+ .disable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build()
+ .readerFor(Month.class);
+ }
+
+ private ObjectReader readerForOneBased() {
+ return JsonMapper.builder()
+ .enable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build()
+ .readerFor(Month.class);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java
new file mode 100644
index 0000000000..c915148553
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/PeriodDeserTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Period;
+import java.time.temporal.TemporalAmount;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.type.LogicalType;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class PeriodDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ @Test
+ public void testDeserialization01() throws Exception
+ {
+ Period period = Period.of(1, 6, 15);
+ Period value = MAPPER.readValue('"' + period.toString() + '"', Period.class);
+ assertEquals(period, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserialization02() throws Exception
+ {
+ Period period = Period.of(0, 0, 21);
+ Period value = MAPPER.readValue('"' + period.toString() + '"', Period.class);
+ assertEquals(period, value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo01() throws Exception
+ {
+ Period period = Period.of(5, 1, 12);
+
+ final ObjectMapper mapper = mapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ TemporalAmount value = mapper.readValue(
+ "[\"" + Period.class.getName() + "\",\"" + period.toString() + "\"]", TemporalAmount.class
+ );
+
+ assertNotNull(value, "The value should not be null.");
+ assertInstanceOf(Period.class, value, "The value should be a Period.");
+ assertEquals(period, value, "The value is not correct.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "period";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ Period actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ Period actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "period";
+ final ObjectMapper mapper = mapperBuilder()
+ .withCoercionConfig(LogicalType.DateTime,
+ cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", ""));
+ try {
+ objectReader.readValue(valueFromEmptyStr);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ }
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java
new file mode 100644
index 0000000000..13a3272860
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearDeserTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.Year;
+import java.time.temporal.Temporal;
+import java.util.Map;
+import java.util.Objects;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class YearDeserTest extends DateTimeTestBase
+{
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ static class FormattedYear {
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "'Y'yyyy")
+ public Year value;
+
+ protected FormattedYear() {}
+ public FormattedYear(Year year) {
+ value = year;
+ }
+ }
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(Year.class);
+
+ @Test
+ public void testDeserializationAsString01()
+ {
+ assertEquals(Year.of(2000), READER.readValue(q("2000")),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testBadDeserializationAsString01()
+ {
+ try {
+ READER.readValue(q("notayear"));
+ fail("expected DateTimeParseException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Year` from String \"notayear\"");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled()
+ {
+ try {
+ read("['2000']");
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled()
+ {
+ try {
+ read("[]");
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array");
+ }
+ try {
+ READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[]");
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.Year` from Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ Year value= newMapper()
+ .readerFor(Year.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue(a2q("['2000']"));
+ assertEquals(Year.of(2000), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ Year value = newMapper()
+ .readerFor(Year.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ @Test
+ public void testDefaultDeserialization() throws Exception
+ {
+ Year value = READER.readValue("1986");
+ assertEquals(Year.of(1986), value, "The value is not correct.");
+ value = READER.readValue("2013");
+ assertEquals(Year.of(2013), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ Temporal value = mapper.readValue("[\"" + Year.class.getName() + "\",2005]", Temporal.class);
+ assertTrue(value instanceof Year, "The value should be a Year.");
+ assertEquals(Year.of(2005), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testWithCustomFormat() throws Exception
+ {
+ FormattedYear input = new FormattedYear(Year.of(2018));
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals("{\"value\":\"Y2018\"}", json);
+ FormattedYear result = MAPPER.readValue(json, FormattedYear.class);
+ assertEquals(input.value, result.value);
+ }
+
+ @Test
+ public void testWithFormatViaConfigOverride() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(Year.class,
+ vc -> vc.setFormat((JsonFormat.Value.forPattern("'X'yyyy"))))
+ .build();
+ Year input = Year.of(2018);
+ String json = mapper.writeValueAsString(input);
+ assertEquals("\"X2018\"", json);
+ Year result = mapper.readValue(json, Year.class);
+ assertEquals(input, result);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for specific issues
+ /**********************************************************
+ */
+
+ // [module-java8#78]
+ final static class ObjectTest {
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "'Y'yyyy")
+ public Year value;
+
+ protected ObjectTest() { }
+ public ObjectTest(Year y) {
+ value = y;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ ObjectTest other = (ObjectTest) o;
+ return Objects.equals(this.value, other.value);
+ }
+
+ // stupid Javac 8 barfs on override missing?!
+ @Override
+ public int hashCode() { return 42; }
+ }
+
+ // [module-java8#78]
+ @Test
+ public void testWithCustomFormat78() throws Exception
+ {
+ ObjectTest input = new ObjectTest(Year.of(2018));
+ String json = MAPPER.writeValueAsString(input);
+ assertEquals("{\"value\":\"Y2018\"}", json);
+ ObjectTest result = MAPPER.readValue(json, ObjectTest.class);
+ assertEquals(input, result);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ // minor changes in 2.12
+ @Test
+ public void testDeserializeFromEmptyString() throws Exception
+ {
+ final String key = "Year";
+ final ObjectReader objectReader = MAPPER.readerFor(MAP_TYPE_REF);
+
+ // First: by default, lenient, so empty String fine
+ String doc = MAPPER.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(doc);
+ assertNull(actualMapFromNullStr.get(key));
+
+ doc = MAPPER.writeValueAsString(asMap("date", ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(doc);
+ assertNotNull(actualMapFromEmptyStr);
+
+ // But can make strict:
+ final ObjectMapper strictMapper = mapperBuilder()
+ .withConfigOverride(Year.class, o -> o.setFormat(
+ JsonFormat.Value.forLeniency(false)))
+ .build();
+ doc = strictMapper.writeValueAsString(asMap("date", ""));
+ try {
+ actualMapFromEmptyStr = strictMapper.readerFor(MAP_TYPE_REF)
+ .readValue(doc);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "not allowed because 'strict' mode set for");
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Helper methods
+ /**********************************************************
+ */
+
+ private Year read(final String json) {
+ return READER.readValue(a2q(json));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java
new file mode 100644
index 0000000000..19023dd701
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/YearMonthDeserTest.java
@@ -0,0 +1,145 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.io.IOException;
+import java.time.Month;
+import java.time.YearMonth;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class YearMonthDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(YearMonth.class);
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ @Test
+ public void testDeserializationAsString01() throws Exception
+ {
+ final YearMonth value = read("'2000-01'");
+ assertEquals(YearMonth.of(2000, Month.JANUARY), value,
+ "The value is not correct");
+ }
+
+ @Test
+ public void testBadDeserializationAsString01() throws Exception
+ {
+ try {
+ read(q("notayearmonth"));
+ fail("expected DateTimeParseException");
+ } catch (InvalidFormatException e) {
+ verifyException(e, "could not be parsed");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Exception
+ {
+ try {
+ read("['2000-01']");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e,
+"Unexpected token (`JsonToken.VALUE_STRING`), expected `JsonToken.VALUE_NUMBER_INT`");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Exception
+ {
+ // works even without the feature enabled
+ assertNull(read("[]"));
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Exception
+ {
+ YearMonth value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue(a2q("['2000-01']"));
+ assertEquals(YearMonth.of(2000, Month.JANUARY), value,
+ "The value is not correct");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Exception
+ {
+ YearMonth value = READER.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue( "[]");
+ assertNull(value);
+ }
+
+ // [modules-java8#249]
+ @Test
+ public void testYearAbove10k() throws Exception
+ {
+ YearMonth input = YearMonth.of(10000, 1);
+ String json = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(input);
+ YearMonth result = READER.readValue(json);
+ assertEquals(input, result);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "yearMonth";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String dateValAsEmptyStr = "";
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ YearMonth actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, dateValAsEmptyStr));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ YearMonth actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertNull(actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "YearMonth";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(YearMonth.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", ""));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ private YearMonth read(final String json) throws IOException {
+ return READER.readValue(a2q(json));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java
new file mode 100644
index 0000000000..3578189584
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneIdDeserTest.java
@@ -0,0 +1,140 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.ZoneId;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.type.LogicalType;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ZoneIdDeserTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ private final ObjectMapper MOCK_OBJECT_MIXIN_MAPPER = mapperBuilder()
+ .addMixIn(ZoneId.class, MockObjectConfiguration.class)
+ .build();
+
+ @Test
+ public void testSimpleZoneIdDeser()
+ {
+ assertEquals(ZoneId.of("America/Chicago"),
+ MAPPER.readValue("\"America/Chicago\"", ZoneId.class));
+ assertEquals(ZoneId.of("America/Anchorage"),
+ MAPPER.readValue("\"America/Anchorage\"", ZoneId.class));
+ }
+
+ @Test
+ public void testPolymorphicZoneIdDeser()
+ {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addMixIn(ZoneId.class, MockObjectConfiguration.class)
+ .build();
+ ZoneId value = mapper.readValue("[\"" + ZoneId.class.getName() + "\",\"America/Denver\"]", ZoneId.class);
+ assertEquals(ZoneId.of("America/Denver"), value);
+ }
+
+ @Test
+ public void testDeserialization01()
+ {
+ assertEquals(ZoneId.of("America/Chicago"),
+ MAPPER.readValue("\"America/Chicago\"", ZoneId.class));
+ }
+
+ @Test
+ public void testDeserialization02()
+ {
+ assertEquals(ZoneId.of("America/Anchorage"),
+ MAPPER.readValue("\"America/Anchorage\"", ZoneId.class));
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo02()
+ {
+ ZoneId value = MOCK_OBJECT_MIXIN_MAPPER.readValue("[\"" + ZoneId.class.getName() + "\",\"America/Denver\"]", ZoneId.class);
+ assertEquals(ZoneId.of("America/Denver"), value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString()
+ {
+
+ String key = "zoneId";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ ZoneId actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ ZoneId actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr,
+ "empty string failed to deserialize to null with lenient setting");
+ }
+
+ public void testStrictDeserializeFromEmptyString()
+ {
+
+ final String key = "zoneId";
+ final ObjectMapper mapper = mapperBuilder()
+ .withCoercionConfig(LogicalType.DateTime,
+ cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ try {
+ objectReader.readValue(valueFromEmptyStr);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ verifyException(e, ZoneId.class.getName());
+ }
+ }
+
+ // [module-java8#68]
+ @Test
+ public void testZoneIdDeserFromEmpty()
+ {
+ // by default, should be fine
+ assertNull(MAPPER.readValue(q(" "), ZoneId.class));
+ // but fail if coercion illegal
+ final ObjectMapper mapper = mapperBuilder()
+ .withCoercionConfig(LogicalType.DateTime,
+ cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail))
+ .build();
+ try {
+ mapper.readValue(q(" "), ZoneId.class);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ verifyException(e, ZoneId.class.getName());
+ }
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java
new file mode 100644
index 0000000000..7fdd1421c9
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZoneOffsetDeserTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.CoercionAction;
+import tools.jackson.databind.cfg.CoercionInputShape;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.type.LogicalType;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ZoneOffsetDeserTest extends DateTimeTestBase
+{
+ private final static ObjectMapper MAPPER = newMapper();
+ private final static ObjectReader READER = MAPPER.readerFor(ZoneOffset.class);
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ @Test
+ public void testSimpleZoneOffsetDeser() throws Exception
+ {
+ assertEquals(ZoneOffset.of("Z"), READER.readValue("\"Z\""),
+ "The value is not correct.");
+ assertEquals(ZoneOffset.of("+0300"), READER.readValue(q("+0300")),
+ "The value is not correct.");
+ assertEquals(ZoneOffset.of("-0630"), READER.readValue("\"-06:30\""),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testPolymorphicZoneOffsetDeser() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(ZoneId.class, MockObjectConfiguration.class)
+ .build();
+ ZoneId value = mapper.readValue("[\"" + ZoneOffset.class.getName() + "\",\"+0415\"]", ZoneId.class);
+ assertTrue(value instanceof ZoneOffset);
+ assertEquals(ZoneOffset.of("+0415"), value);
+ }
+
+ @Test
+ public void testDeserializationWithTypeInfo03() throws Exception
+ {
+ ObjectMapper mapper = mapperBuilder()
+ .addMixIn(ZoneId.class, MockObjectConfiguration.class)
+ .build();
+ ZoneId value = mapper.readValue("[\"" + ZoneOffset.class.getName() + "\",\"+0415\"]", ZoneId.class);
+ assertTrue(value instanceof ZoneOffset, "The value should be a ZoneOffset.");
+ assertEquals(ZoneOffset.of("+0415"), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testBadDeserializationAsString01() throws Throwable
+ {
+ try {
+ READER.readValue("\"notazonedoffset\"");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Invalid ID for ZoneOffset");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[\"+0300\"]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value");
+ }
+ try {
+ READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZoneOffset` from Array value");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ ZoneOffset value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[\"+0300\"]");
+ assertEquals(ZoneOffset.of("+0300"), value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ ZoneOffset value = READER
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "zoneOffset";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ ZoneId actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ ZoneId actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "zoneOffset";
+ final ObjectMapper mapper = mapperBuilder()
+ .withCoercionConfig(LogicalType.DateTime,
+ cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ try {
+ objectReader.readValue(valueFromEmptyStr);
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ verifyException(e, ZoneOffset.class.getName());
+ }
+ }
+
+ // [module-java8#68]
+ @Test
+ public void testZoneOffsetDeserFromEmpty() throws Exception
+ {
+ // by default, should be fine
+ assertNull(MAPPER.readValue(q(" "), ZoneOffset.class));
+ // but fail if coercion illegal
+ final ObjectMapper mapper = mapperBuilder()
+ .withCoercionConfig(LogicalType.DateTime,
+ cfg -> cfg.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail))
+ .build();
+ try {
+ mapper.readerFor(ZoneOffset.class)
+ .readValue(q(" "));
+ fail("Should not pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot coerce empty String");
+ verifyException(e, ZoneOffset.class.getName());
+ }
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java
new file mode 100644
index 0000000000..a38124ddc0
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/ZonedDateTimeDeserTest.java
@@ -0,0 +1,310 @@
+package tools.jackson.databind.ext.javatime.deser;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonFormat.Feature;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.exc.MismatchedInputException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ZonedDateTimeDeserTest extends DateTimeTestBase
+{
+ private final ObjectReader READER = newMapper().readerFor(ZonedDateTime.class);
+
+ private final ObjectReader READER_NON_NORMALIZED_ZONEID = JsonMapper.builder()
+ .disable(DateTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID)
+ .build()
+ .readerFor(ZonedDateTime.class);
+
+ private final TypeReference> MAP_TYPE_REF = new TypeReference>() { };
+
+ static class WrapperWithFeatures {
+ @JsonFormat(without = JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ public ZonedDateTime value;
+ }
+
+ static class WrapperWithReadTimestampsAsNanosDisabled {
+ @JsonFormat(
+ without=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public ZonedDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosDisabled() { }
+ public WrapperWithReadTimestampsAsNanosDisabled(ZonedDateTime v) { value = v; }
+ }
+
+ static class WrapperWithReadTimestampsAsNanosEnabled {
+ @JsonFormat(
+ with=Feature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ public ZonedDateTime value;
+
+ public WrapperWithReadTimestampsAsNanosEnabled() { }
+ public WrapperWithReadTimestampsAsNanosEnabled(ZonedDateTime v) { value = v; }
+ }
+
+ @Test
+ public void testDeserFromString() throws Exception
+ {
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC),
+ READER.readValue(q("2000-01-01T12:00Z")),
+ "The value is not correct.");
+ }
+
+ // [modules-java#281]
+ @Test
+ public void testDeserFromStringNoZoneIdNormalization() throws Exception
+ {
+ // 11-Nov-2023, tatu: Not sure this is great test but... does show diff
+ // behavior with and without `JavaTimeFeature.NORMALIZE_DESERIALIZED_ZONE_ID`
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, TimeZone.getTimeZone("UTC").toZoneId()),
+ READER_NON_NORMALIZED_ZONEID.readValue(q("2000-01-01T12:00Z")),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt01() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosDisabled.class);
+ ZonedDateTime date = ZonedDateTime.of(
+ LocalDateTime.ofEpochSecond(1, 1000000, ZoneOffset.UTC),
+ ZoneOffset.UTC);
+ WrapperWithReadTimestampsAsNanosDisabled actual =
+ reader.readValue(a2q("{'value':1001}"));
+ assertEquals(date, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsInt02() throws Exception
+ {
+ ObjectReader reader = newMapper().readerFor(WrapperWithReadTimestampsAsNanosEnabled.class);
+ ZonedDateTime date = ZonedDateTime.of(
+ LocalDateTime.ofEpochSecond(1, 0, ZoneOffset.UTC),
+ ZoneOffset.UTC);
+ WrapperWithReadTimestampsAsNanosEnabled actual =
+ reader.readValue(a2q("{'value':1}"));
+ assertEquals(date, actual.value, "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationComparedToStandard() throws Throwable
+ {
+ String inputString = "2021-02-01T19:49:04.0513486Z";
+
+ assertEquals(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from),
+ READER.readValue(q(inputString)),
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationComparedToStandard2() throws Throwable
+ {
+ String inputString = "2021-02-01T19:49:04.0513486Z[UTC]";
+
+ ZonedDateTime converted = newMapperBuilder()
+ .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
+ .build()
+ .readerFor(ZonedDateTime.class).readValue(q(inputString));
+
+ assertEquals(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(inputString, ZonedDateTime::from),
+ converted,
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testBadDeserializationAsString01() throws Throwable
+ {
+ try {
+ READER.readValue(q("notazone"));
+ fail("Should nae pass");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from String");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[\"2000-01-01T12:00Z\"]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayDisabled() throws Throwable
+ {
+ try {
+ READER.readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array");
+ }
+ try {
+ newMapper()
+ .readerFor(ZonedDateTime.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[]");
+ fail("expected MismatchedInputException");
+ } catch (MismatchedInputException e) {
+ verifyException(e, "Cannot deserialize value of type `java.time.ZonedDateTime` from Array");
+ }
+ }
+
+ @Test
+ public void testDeserializationAsArrayEnabled() throws Throwable
+ {
+ ZonedDateTime value = newMapper()
+ .readerFor(ZonedDateTime.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+ .readValue("[\"2000-01-01T12:00Z\"]");
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC),
+ value,
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testDeserializationAsEmptyArrayEnabled() throws Throwable
+ {
+ ZonedDateTime value = newMapper()
+ .readerFor(ZonedDateTime.class)
+ .with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS,
+ DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
+ .readValue("[]");
+ assertNull(value);
+ }
+
+ @Test
+ public void testDeserializationWithZonePreserved() throws Throwable
+ {
+ WrapperWithFeatures wrapper = newMapper()
+ .readerFor(WrapperWithFeatures.class)
+ .readValue("{\"value\":\"2000-01-01T12:00+01:00\"}");
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)),
+ wrapper.value,
+ "Timezone should be preserved.");
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for empty string handling
+ /**********************************************************
+ */
+
+ @Test
+ public void testLenientDeserializeFromEmptyString() throws Exception {
+
+ String key = "zoneDateTime";
+ ObjectMapper mapper = newMapper();
+ ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ ZonedDateTime actualDateFromNullStr = actualMapFromNullStr.get(key);
+ assertNull(actualDateFromNullStr);
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ Map actualMapFromEmptyStr = objectReader.readValue(valueFromEmptyStr);
+ ZonedDateTime actualDateFromEmptyStr = actualMapFromEmptyStr.get(key);
+ assertEquals(null, actualDateFromEmptyStr, "empty string failed to deserialize to null with lenient setting");
+ }
+
+ @Test
+ public void testStrictDeserializeFromEmptyString() throws Exception {
+
+ final String key = "zonedDateTime";
+ final ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(ZonedDateTime.class,
+ o -> o.setFormat(JsonFormat.Value.forLeniency(false)))
+ .build();
+ final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
+
+ String valueFromNullStr = mapper.writeValueAsString(asMap(key, null));
+ Map actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
+ assertNull(actualMapFromNullStr.get(key));
+
+ String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
+ assertThrows(MismatchedInputException.class, () -> objectReader.readValue(valueFromEmptyStr));
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for ISO-8601 ZonedDateTimes that are colonless
+ /**********************************************************
+ */
+
+ @Test
+ public void testDeserializationWithoutColonInOffset() throws Throwable
+ {
+ WrapperWithFeatures wrapper = READER
+ .forType(WrapperWithFeatures.class)
+ .readValue("{\"value\":\"2000-01-01T12:00+0100\"}");
+
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)),
+ wrapper.value,
+ "Value parses as if it were with colon");
+ }
+
+ @Test
+ public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable
+ {
+ WrapperWithFeatures wrapper = READER
+ .forType(WrapperWithFeatures.class)
+ .readValue("{\"value\":\"2000-01-01T12:00+0100[Europe/Paris]\"}");
+ assertEquals(ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneId.of("Europe/Paris")),
+ wrapper.value,
+ "Timezone should be preserved.");
+ }
+
+ @Test
+ public void ZonedDateTime_with_offset_can_be_deserialized() throws Exception {
+ ObjectReader r = newMapper().readerFor(ZonedDateTime.class)
+ .without(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+
+ String base = "2015-07-24T12:23:34.184";
+ for (String offset : Arrays.asList("+00", "-00")) {
+ String time = base + offset;
+ if (!System.getProperty("java.version").startsWith("1.8")) {
+ // JDK 8 cannot parse hour offsets without minutes
+ assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + '"'));
+ }
+ assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + "00" + '"'));
+ assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184Z"), r.readValue('"' + time + ":00" + '"'));
+ assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + "30" + '"'));
+ assertEquals(ZonedDateTime.parse("2015-07-24T12:23:34.184" + offset + ":30" ), r.readValue('"' + time + ":30" + '"'));
+ }
+
+ for (String prefix : Arrays.asList("-", "+")) {
+ for (String hours : Arrays.asList("00", "01", "02", "03", "11", "12")) {
+ String time = base + prefix + hours;
+ ZonedDateTime expectedHour = ZonedDateTime.parse(time + ":00");
+ if (!System.getProperty("java.version").startsWith("1.8")) {
+ // JDK 8 cannot parse hour offsets without minutes
+ assertEquals(expectedHour, r.readValue('"' + time + '"'));
+ }
+ assertEquals(expectedHour, r.readValue('"' + time + "00" + '"'));
+ assertEquals(expectedHour, r.readValue('"' + time + ":00" + '"'));
+ assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + "30" + '"'));
+ assertEquals(ZonedDateTime.parse(time + ":30"), r.readValue('"' + time + ":30" + '"'));
+ }
+ }
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java
new file mode 100644
index 0000000000..a397b376c2
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/deser/key/ZonedDateTimeKeyDeserializerTest.java
@@ -0,0 +1,59 @@
+package tools.jackson.databind.ext.javatime.deser.key;
+
+import java.time.ZonedDateTime;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// for [modules-java8#306]
+public class ZonedDateTimeKeyDeserializerTest
+ extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final TypeReference> MAP_TYPE_REF
+ = new TypeReference>() {};
+
+ @Test
+ public void Instant_style_can_be_deserialized() throws Exception {
+ Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z"),
+ MAP_TYPE_REF);
+ Map.Entry entry = map.entrySet().iterator().next();
+ assertEquals("2015-07-24T12:23:34.184Z", entry.getKey().toString());
+ }
+
+ @Test
+ public void ZonedDateTime_with_zone_name_can_be_deserialized() throws Exception {
+ Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z[UTC]"),
+ MAP_TYPE_REF);
+ Map.Entry entry = map.entrySet().iterator().next();
+ assertEquals("2015-07-24T12:23:34.184Z[UTC]", entry.getKey().toString());
+ }
+
+ // NOTE: Java 9+ test
+ @Test
+ public void ZonedDateTime_with_place_name_can_be_deserialized() throws Exception {
+ Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184Z[Europe/London]"),
+ MAP_TYPE_REF);
+ Map.Entry entry = map.entrySet().iterator().next();
+ assertEquals("2015-07-24T13:23:34.184+01:00[Europe/London]", entry.getKey().toString());
+ }
+
+ @Test
+ public void ZonedDateTime_with_offset_can_be_deserialized() throws Exception {
+ Map map = MAPPER.readValue(getMap("2015-07-24T12:23:34.184+02:00"),
+ MAP_TYPE_REF);
+ Map.Entry entry = map.entrySet().iterator().next();
+ assertEquals("2015-07-24T12:23:34.184+02:00", entry.getKey().toString());
+ }
+
+ private static String getMap(String input) {
+ return "{\"" + input + "\": \"This is a string\"}";
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java
new file mode 100644
index 0000000000..b925e88653
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/DurationAsKeyTest.java
@@ -0,0 +1,34 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DurationAsKeyTest extends DateTimeTestBase
+{
+ private static final Duration DURATION = Duration.ofMinutes(13).plusSeconds(37).plusNanos(120 * 1000 * 1000L);
+ private static final String DURATION_STRING = "PT13M37.12S";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { });
+
+ @Test
+ public void testSerialization() throws Exception {
+ assertEquals(mapAsString(DURATION_STRING, "test"),
+ MAPPER.writeValueAsString(Collections.singletonMap(DURATION, "test")));
+ }
+
+ @Test
+ public void testDeserialization() throws Exception {
+ assertEquals(Collections.singletonMap(DURATION, "test"), READER.readValue(mapAsString(DURATION_STRING, "test")));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java
new file mode 100644
index 0000000000..ebb9c85c40
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/InstantAsKeyTest.java
@@ -0,0 +1,50 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.Instant;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class InstantAsKeyTest extends DateTimeTestBase
+{
+ private static final Instant INSTANT_0 = Instant.ofEpochMilli(0);
+ private static final String INSTANT_0_STRING = "1970-01-01T00:00:00Z";
+ private static final Instant INSTANT = Instant.ofEpochSecond(1426325213l, 590000000l);
+ private static final String INSTANT_STRING = "2015-03-14T09:26:53.590Z";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { });
+
+ @Test
+ public void testSerialization0() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(INSTANT_0, "test"));
+ assertEquals(mapAsString(INSTANT_0_STRING, "test"), value);
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(INSTANT, "test"));
+ assertEquals(mapAsString(INSTANT_STRING, "test"), value);
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ Map value = READER.readValue(mapAsString(INSTANT_0_STRING, "test"));
+ Map EXP = asMap(INSTANT_0, "test");
+ assertEquals(EXP, value, "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ Map value = READER.readValue(mapAsString(INSTANT_STRING, "test"));
+ Map EXP = asMap(INSTANT, "test");
+ assertEquals(EXP, value, "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java
new file mode 100644
index 0000000000..2d062e1fab
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateAsKeyTest.java
@@ -0,0 +1,33 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateAsKeyTest extends DateTimeTestBase
+{
+ private static final LocalDate DATE = LocalDate.of(2015, 3, 14);
+ private static final String DATE_STRING = "2015-03-14";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { });
+
+ @Test
+ public void testSerialization() throws Exception {
+ assertEquals(mapAsString(DATE_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(DATE, "test")));
+ }
+
+ @Test
+ public void testDeserialization() throws Exception {
+ assertEquals(asMap(DATE, "test"), READER.readValue(mapAsString(DATE_STRING, "test")));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java
new file mode 100644
index 0000000000..a6bc233b19
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalDateTimeAsKeyTest.java
@@ -0,0 +1,80 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.deser.DeserializationProblemHandler;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateTimeAsKeyTest extends DateTimeTestBase
+{
+ private static final LocalDateTime DATE_TIME_0 = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC);
+ /*
+ * Current serializer is LocalDateTime.toString(), which omits seconds if it can
+ */
+ private static final String DATE_TIME_0_STRING = "1970-01-01T00:00";
+ private static final LocalDateTime DATE_TIME = LocalDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000);
+ private static final String DATE_TIME_STRING = "2015-03-14T09:26:53.590";
+
+ private final TypeReference> TYPE_REF = new TypeReference>() { };
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(DATE_TIME_0, "test"));
+ assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value);
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(DATE_TIME, "test"));
+ assertEquals(mapAsString(DATE_TIME_STRING, "test"), value);
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ Map value = READER.readValue(
+ mapAsString(DATE_TIME_0_STRING, "test"));
+ assertEquals(asMap(DATE_TIME_0, "test"), value, "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ Map value = READER.readValue(
+ mapAsString(DATE_TIME_STRING, "test"));
+ assertEquals(asMap(DATE_TIME, "test"), value, "Value is incorrect");
+ }
+
+ @Test
+ public void testDateTimeExceptionIsHandled() throws Throwable
+ {
+ LocalDateTime now = LocalDateTime.now();
+ DeserializationProblemHandler handler = new DeserializationProblemHandler() {
+ @Override
+ public Object handleWeirdKey(DeserializationContext ctxt, Class> targetType,
+ String valueToConvert, String failureMsg) {
+ if (LocalDateTime.class == targetType) {
+ if ("now".equals(valueToConvert)) {
+ return now;
+ }
+ }
+ return NOT_HANDLED;
+ }
+ };
+ Map value = mapperBuilder().addHandler(handler)
+ .build()
+ .readValue(mapAsString("now", "test"), TYPE_REF);
+
+ assertEquals(asMap(now, "test"), value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java
new file mode 100644
index 0000000000..f3fe7c62b1
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/LocalTimeAsKeyTest.java
@@ -0,0 +1,53 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.LocalTime;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalTimeAsKeyTest extends DateTimeTestBase
+{
+ private static final LocalTime TIME_0 = LocalTime.ofSecondOfDay(0);
+ /*
+ * Seconds are omitted if possible
+ */
+ private static final String TIME_0_STRING = "00:00";
+ private static final LocalTime TIME = LocalTime.of(3, 14, 15, 920 * 1000 * 1000);
+ private static final String TIME_STRING = "03:14:15.920";
+
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ assertEquals(mapAsString(TIME_0_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(TIME_0, "test")));
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ assertEquals(mapAsString(TIME_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(TIME, "test")));
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(TIME_0, "test"), READER.readValue(mapAsString(TIME_0_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(TIME, "test"), READER.readValue(mapAsString(TIME_STRING, "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java
new file mode 100644
index 0000000000..28f1d9b3ae
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/MonthDayAsKeyTest.java
@@ -0,0 +1,36 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.MonthDay;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class MonthDayAsKeyTest extends DateTimeTestBase
+{
+ private static final MonthDay MONTH_DAY = MonthDay.of(3, 14);
+ private static final String MONTH_DAY_STRING = "--03-14";
+
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization() throws Exception {
+ assertEquals(mapAsString(MONTH_DAY_STRING, "test"), MAPPER.writeValueAsString(asMap(MONTH_DAY, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization() throws Exception {
+ assertEquals(asMap(MONTH_DAY, "test"), READER.readValue(mapAsString(MONTH_DAY_STRING, "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java
new file mode 100644
index 0000000000..42d03deadf
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetDateTimeAsKeyTest.java
@@ -0,0 +1,70 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OffsetDateTimeAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private static final OffsetDateTime DATE_TIME_0 = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0), ZoneOffset.UTC);
+ private static final String DATE_TIME_0_STRING = "1970-01-01T00:00Z";
+ private static final OffsetDateTime DATE_TIME_1 = OffsetDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.UTC);
+ private static final String DATE_TIME_1_STRING = "2015-03-14T09:26:53.590Z";
+ private static final OffsetDateTime DATE_TIME_2 = OffsetDateTime.of(2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.ofHours(6));
+ private static final String DATE_TIME_2_STRING = "2015-03-14T09:26:53.590+06:00";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(DATE_TIME_0, "test"));
+ assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value,
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ assertEquals(mapAsString(DATE_TIME_1_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(DATE_TIME_1, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testSerialization2() throws Exception {
+ assertEquals(mapAsString(DATE_TIME_2_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(DATE_TIME_2, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(DATE_TIME_0, "test"), READER.readValue(mapAsString(DATE_TIME_0_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(DATE_TIME_1, "test"), READER.readValue(mapAsString(DATE_TIME_1_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization2() throws Exception {
+ assertEquals(asMap(DATE_TIME_2, "test"),
+ READER.readValue(mapAsString(DATE_TIME_2_STRING, "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java
new file mode 100644
index 0000000000..381e2c5e91
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/OffsetTimeAsKeyTest.java
@@ -0,0 +1,70 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OffsetTimeAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private static final OffsetTime TIME_0 = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC);
+ private static final String TIME_0_STRING = "00:00Z";
+ private static final OffsetTime TIME_1 = OffsetTime.of(3, 14, 15, 920 * 1000 * 1000, ZoneOffset.UTC);
+ private static final String TIME_1_STRING = "03:14:15.920Z";
+ private static final OffsetTime TIME_2 = OffsetTime.of(3, 14, 15, 920 * 1000 * 1000, ZoneOffset.ofHours(6));
+ private static final String TIME_2_STRING = "03:14:15.920+06:00";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ assertEquals(mapAsString(TIME_0_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(TIME_0, "test")));
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ assertEquals(mapAsString(TIME_1_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(TIME_1, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testSerialization2() throws Exception {
+ assertEquals(mapAsString(TIME_2_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(TIME_2, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(TIME_0, "test"),
+ READER.readValue(mapAsString(TIME_0_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(TIME_1, "test"),
+ READER.readValue(mapAsString(TIME_1_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization2() throws Exception {
+ assertEquals(asMap(TIME_2, "test"),
+ READER.readValue(mapAsString(TIME_2_STRING, "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java
new file mode 100644
index 0000000000..22b9b64071
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/PeriodAsKeyTest.java
@@ -0,0 +1,54 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.Period;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class PeriodAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private static final Period PERIOD_0 = Period.of(0, 0, 0);
+ private static final String PERIOD_0_STRING = "P0D";
+ private static final Period PERIOD = Period.of(3, 1, 4);
+ private static final String PERIOD_STRING = "P3Y1M4D";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ assertEquals(mapAsString(PERIOD_0_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(PERIOD_0, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ assertEquals(mapAsString(PERIOD_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(PERIOD, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(PERIOD_0, "test"),
+ READER.readValue(mapAsString(PERIOD_0_STRING, "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(PERIOD, "test"),
+ READER.readValue(mapAsString(PERIOD_STRING, "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java
new file mode 100644
index 0000000000..c14d5d222a
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/YearAsKeyTest.java
@@ -0,0 +1,70 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.Year;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.exc.InvalidFormatException;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class YearAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testKeySerialization() throws Exception {
+ assertEquals(mapAsString("3141", "test"),
+ MAPPER.writeValueAsString(asMap(Year.of(3141), "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testKeyDeserialization() throws Exception {
+ assertEquals(asMap(Year.of(3141), "test"), READER.readValue(mapAsString("3141", "test")),
+ "Value is incorrect");
+ // Test both padded, unpadded
+ assertEquals(asMap(Year.of(476), "test"), READER.readValue(mapAsString("0476", "test")),
+ "Value is incorrect");
+ assertEquals(asMap(Year.of(476), "test"), READER.readValue(mapAsString("476", "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void deserializeYearKey_notANumber() throws Exception {
+ assertThrows(InvalidFormatException.class, () -> {
+ READER.readValue(mapAsString("10000BC", "test"));
+ });
+ }
+
+ @Test
+ public void deserializeYearKey_notAYear() throws Exception {
+ assertThrows(InvalidFormatException.class, () -> {
+ READER.readValue(mapAsString(Integer.toString(Year.MAX_VALUE+1), "test"));
+ });
+ }
+
+ @Test
+ public void serializeAndDeserializeYearKeyUnpadded() throws Exception {
+ // fix for issue #51 verify we can deserialize an unpadded year e.g. "1"
+ Map testMap = Collections.singletonMap(Year.of(1), 1F);
+ String serialized = MAPPER.writeValueAsString(testMap);
+ TypeReference> yearFloatTypeReference = new TypeReference>() {};
+ Map deserialized = MAPPER.readValue(serialized, yearFloatTypeReference);
+ assertEquals(testMap, deserialized);
+
+ // actually, check padded as well just to make sure
+ Map deserialized2 = MAPPER.readValue(a2q("{'0001':1.0}"),
+ yearFloatTypeReference);
+ assertEquals(testMap, deserialized2);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java
new file mode 100644
index 0000000000..03bee96d1a
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/YearMonthAsKeyTest.java
@@ -0,0 +1,34 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.YearMonth;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class YearMonthAsKeyTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() {
+ });
+
+ @Test
+ public void testSerialization() throws Exception {
+ assertEquals(mapAsString("3141-05", "test"),
+ MAPPER.writeValueAsString(asMap(YearMonth.of(3141, 5), "test")),
+ "Value is incorrect");
+ }
+
+ @Test
+ public void testDeserialization() throws Exception {
+ assertEquals(asMap(YearMonth.of(3141, 5), "test"),
+ READER.readValue(mapAsString("3141-05", "test")),
+ "Value is incorrect");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java
new file mode 100644
index 0000000000..66e071d399
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneIdAsKeyTest.java
@@ -0,0 +1,62 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.ZoneId;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ZoneIdAsKeyTest extends DateTimeTestBase
+{
+ private static final ZoneId ZONE_0 = ZoneId.of("UTC");
+ private static final String ZONE_0_STRING = "UTC";
+ private static final ZoneId ZONE_1 = ZoneId.of("+06:00");
+ private static final String ZONE_1_STRING = "+06:00";
+ private static final ZoneId ZONE_2 = ZoneId.of("Europe/London");
+ private static final String ZONE_2_STRING = "Europe/London";
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(new TypeReference>() { });
+
+ @Test
+ public void testSerialization0() throws Exception {
+ assertEquals(mapAsString(ZONE_0_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(ZONE_0, "test")));
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ assertEquals(mapAsString(ZONE_1_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(ZONE_1, "test")));
+ }
+
+ @Test
+ public void testSerialization2() throws Exception {
+ assertEquals(mapAsString(ZONE_2_STRING, "test"),
+ MAPPER.writeValueAsString(asMap(ZONE_2, "test")));
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(ZONE_0, "test"),
+ READER.readValue(mapAsString(ZONE_0_STRING, "test")));
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(ZONE_1, "test"),
+ READER.readValue(mapAsString(ZONE_1_STRING, "test")));
+ }
+
+ @Test
+ public void testDeserialization2() throws Exception {
+ assertEquals(asMap(ZONE_2, "test"),
+ READER.readValue(mapAsString(ZONE_2_STRING, "test")));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java
new file mode 100644
index 0000000000..bf857040df
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZoneOffsetAsKeyTest.java
@@ -0,0 +1,48 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.ZoneOffset;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ZoneOffsetAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private static final ZoneOffset OFFSET_0 = ZoneOffset.UTC;
+ private static final String OFFSET_0_STRING = "Z";
+ private static final ZoneOffset OFFSET_1 = ZoneOffset.ofHours(6);
+ private static final String OFFSET_1_STRING = "+06:00";
+
+ private final ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerialization0() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(OFFSET_0, "test"));
+ assertEquals(mapAsString(OFFSET_0_STRING, "test"), value);
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ String value = MAPPER.writeValueAsString(asMap(OFFSET_1, "test"));
+ assertEquals(mapAsString(OFFSET_1_STRING, "test"), value);
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ Map value = MAPPER.readValue(mapAsString(OFFSET_0_STRING, "test"), TYPE_REF);
+ assertEquals(asMap(OFFSET_0, "test"), value);
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ Map value = MAPPER.readValue(mapAsString(OFFSET_1_STRING, "test"), TYPE_REF);
+ assertEquals(asMap(OFFSET_1, "test"), value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java b/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java
new file mode 100644
index 0000000000..90912ac382
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/key/ZonedDateTimeAsKeyTest.java
@@ -0,0 +1,90 @@
+package tools.jackson.databind.ext.javatime.key;
+
+import java.time.*;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ZonedDateTimeAsKeyTest extends DateTimeTestBase
+{
+ private static final TypeReference> TYPE_REF = new TypeReference>() {
+ };
+ private static final ZonedDateTime DATE_TIME_0 = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0), ZoneOffset.UTC);
+ private static final String DATE_TIME_0_STRING = "1970-01-01T00:00:00Z";
+// private static final Instant DATE_TIME_0_INSTANT = DATE_TIME_0.toInstant();
+
+ private static final ZonedDateTime DATE_TIME_1 = ZonedDateTime.of(
+ 2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneOffset.UTC);
+ private static final String DATE_TIME_1_STRING = "2015-03-14T09:26:53.59Z";
+
+ private static final ZonedDateTime DATE_TIME_2 = ZonedDateTime.of(
+ 2015, 3, 14, 9, 26, 53, 590 * 1000 * 1000, ZoneId.of("Europe/Budapest"));
+ /**
+ * Value of {@link #DATE_TIME_2} after it's been serialized and read back. Serialization throws away time zone information, it only
+ * keeps offset data.
+ */
+ private static final ZonedDateTime DATE_TIME_2_OFFSET = DATE_TIME_2.withZoneSameInstant(ZoneOffset.ofHours(1));
+ private static final String DATE_TIME_2_STRING = "2015-03-14T09:26:53.59+01:00";;
+
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectReader READER = MAPPER.readerFor(TYPE_REF);
+
+ @Test
+ public void testSerialization0() throws Exception {
+ String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_0, "test"));
+ assertEquals(mapAsString(DATE_TIME_0_STRING, "test"), value);
+ }
+
+ @Test
+ public void testSerialization1() throws Exception {
+ String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test"));
+ assertEquals(mapAsString(DATE_TIME_1_STRING, "test"), value);
+ }
+
+ @Test
+ public void testSerialization2() throws Exception {
+ String value = MAPPER.writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_2, "test"));
+ assertEquals(mapAsString(DATE_TIME_2_STRING, "test"), value);
+ }
+
+ @Test
+ public void testDeserialization0() throws Exception {
+ assertEquals(asMap(DATE_TIME_0, "test"),
+ READER.readValue(mapAsString(DATE_TIME_0_STRING, "test")));
+ }
+
+ @Test
+ public void testDeserialization1() throws Exception {
+ assertEquals(asMap(DATE_TIME_1, "test"),
+ READER.readValue(mapAsString(DATE_TIME_1_STRING, "test")));
+ }
+
+ @Test
+ public void testDeserialization2() throws Exception {
+ assertEquals(asMap(DATE_TIME_2_OFFSET, "test"),
+ READER.readValue(mapAsString(DATE_TIME_2_STRING, "test")));
+ }
+
+ @Test
+ public void testSerializationToInstantWithNanos() throws Exception {
+ String value = mapperBuilder().enable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS).build()
+ .writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test"));
+ assertEquals(mapAsString(String.valueOf(DATE_TIME_1.toEpochSecond()) + '.' + DATE_TIME_1.getNano(), "test"), value);
+ }
+
+ @Test
+ public void testSerializationToInstantWithoutNanos() throws Exception {
+ String value = mapperBuilder().enable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS).build()
+ .writerFor(TYPE_REF).writeValueAsString(asMap(DATE_TIME_1, "test"));
+ assertEquals(mapAsString(String.valueOf(DATE_TIME_1.toInstant().toEpochMilli()), "test"), value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java
new file mode 100644
index 0000000000..e798ed137a
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeExceptionTest.java
@@ -0,0 +1,24 @@
+package tools.jackson.databind.ext.javatime.misc;
+
+import java.time.DateTimeException;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DateTimeExceptionTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+
+ // [modules-java#319]: should not fail to ser/deser DateTimeException
+ @Test
+ public void testDateTimeExceptionRoundtrip() throws Exception
+ {
+ String json = MAPPER.writeValueAsString(new DateTimeException("Test!"));
+ DateTimeException result = MAPPER.readValue(json, DateTimeException.class);
+ assertEquals("Test!", result.getMessage());
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java
new file mode 100644
index 0000000000..57f85867e5
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DateTimeSchemasTest.java
@@ -0,0 +1,239 @@
+package tools.jackson.databind.ext.javatime.misc;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.jsonFormatVisitors.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DateTimeSchemasTest extends DateTimeTestBase
+{
+ static class VisitorWrapper implements JsonFormatVisitorWrapper {
+ SerializationContext serializationContext;
+ final String baseName;
+ final Map traversedProperties;
+
+ public VisitorWrapper(SerializationContext ctxt, String baseName, Map traversedProperties) {
+ this.serializationContext = ctxt;
+ this.baseName = baseName;
+ this.traversedProperties = traversedProperties;
+ }
+
+ VisitorWrapper createSubtraverser(String bn) {
+ return new VisitorWrapper(getContext(), bn, traversedProperties);
+ }
+
+ public Map getTraversedProperties() {
+ return traversedProperties;
+ }
+
+ @Override
+ public JsonObjectFormatVisitor expectObjectFormat(JavaType type) {
+ return new JsonObjectFormatVisitor.Base(serializationContext) {
+ @Override
+ public void property(BeanProperty prop) {
+ anyProperty(prop);
+ }
+
+ @Override
+ public void optionalProperty(BeanProperty prop) {
+ anyProperty(prop);
+ }
+
+ private void anyProperty(BeanProperty prop) {
+ final String propertyName = prop.getFullName().toString();
+ traversedProperties.put(baseName + propertyName, "");
+ serializationContext.findPrimaryPropertySerializer(prop.getType(), prop)
+ .acceptJsonFormatVisitor(createSubtraverser(baseName + propertyName + "."), prop.getType());
+ }
+ };
+ }
+
+ @Override
+ public JsonArrayFormatVisitor expectArrayFormat(JavaType type) {
+ traversedProperties.put(baseName, "ARRAY/"+type.getGenericSignature());
+ return null;
+ }
+
+ @Override
+ public JsonStringFormatVisitor expectStringFormat(JavaType type) {
+ return new JsonStringFormatVisitor.Base() {
+ @Override
+ public void format(JsonValueFormat format) {
+ traversedProperties.put(baseName, "STRING/"+format.name());
+ }
+ };
+ }
+
+ @Override
+ public JsonNumberFormatVisitor expectNumberFormat(JavaType type) {
+ return new JsonNumberFormatVisitor.Base() {
+ @Override
+ public void numberType(JsonParser.NumberType format) {
+ traversedProperties.put(baseName, "NUMBER/"+format.name());
+ }
+ };
+ }
+
+ @Override
+ public JsonIntegerFormatVisitor expectIntegerFormat(JavaType type) {
+ return new JsonIntegerFormatVisitor.Base() {
+ @Override
+ public void numberType(JsonParser.NumberType numberType) {
+ traversedProperties.put(baseName + "numberType", "INTEGER/" + numberType.name());
+ }
+
+ @Override
+ public void format(JsonValueFormat format) {
+ traversedProperties.put(baseName + "format", "INTEGER/" + format.name());
+ }
+ };
+ }
+
+ @Override
+ public JsonBooleanFormatVisitor expectBooleanFormat(JavaType type) {
+ traversedProperties.put(baseName, "BOOLEAN");
+ return new JsonBooleanFormatVisitor.Base();
+ }
+
+ @Override
+ public JsonNullFormatVisitor expectNullFormat(JavaType type) {
+ return new JsonNullFormatVisitor.Base();
+ }
+
+ @Override
+ public JsonAnyFormatVisitor expectAnyFormat(JavaType type) {
+ traversedProperties.put(baseName, "ANY");
+ return new JsonAnyFormatVisitor.Base();
+ }
+
+ @Override
+ public JsonMapFormatVisitor expectMapFormat(JavaType type) {
+ traversedProperties.put(baseName, "MAP");
+ return new JsonMapFormatVisitor.Base(serializationContext);
+ }
+
+ @Override
+ public SerializationContext getContext() {
+ return serializationContext;
+ }
+
+ @Override
+ public void setContext(SerializationContext ctxt) {
+ this.serializationContext = ctxt;
+ }
+ }
+
+ // 05-Feb-2025, tatu: Change defaults to Jackson 2.x wrt serialization
+ // shape (as Timestamps vs Strings)
+ private final ObjectMapper MAPPER = mapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+
+ // // // Local date/time types
+
+ // [modules-java8#105]
+ @Test
+ public void testLocalTimeSchema() throws Exception
+ {
+ VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().acceptJsonFormatVisitor(LocalTime.class, wrapper);
+ Map properties = wrapper.getTraversedProperties();
+
+ // By default, serialized as an int array, so:
+ assertEquals(1, properties.size());
+ _verifyIntArrayType(properties.get(""));
+
+ // but becomes date/time
+ wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .acceptJsonFormatVisitor(LocalTime.class, wrapper);
+ properties = wrapper.getTraversedProperties();
+ _verifyTimeType(properties.get(""));
+ }
+
+ @Test
+ public void testLocalDateSchema() throws Exception
+ {
+ VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().acceptJsonFormatVisitor(LocalDate.class, wrapper);
+ Map properties = wrapper.getTraversedProperties();
+
+ // By default, serialized as an int array, so:
+ assertEquals(1, properties.size());
+ _verifyIntArrayType(properties.get(""));
+
+ // but becomes date/time
+ wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .acceptJsonFormatVisitor(LocalDate.class, wrapper);
+ properties = wrapper.getTraversedProperties();
+ _verifyDateType(properties.get(""));
+ }
+
+ // // // Zoned date/time types
+
+ @Test
+ public void testDateTimeSchema() throws Exception
+ {
+ VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().acceptJsonFormatVisitor(ZonedDateTime.class, wrapper);
+ Map properties = wrapper.getTraversedProperties();
+
+ // By default, serialized as an int array, so:
+ assertEquals(1, properties.size());
+ _verifyBigDecimalType(properties.get(""));
+
+ // but becomes long
+ wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .acceptJsonFormatVisitor(ZonedDateTime.class, wrapper);
+ properties = wrapper.getTraversedProperties();
+ _verifyLongType(properties.get("numberType"));
+ _verifyLongFormat(properties.get("format"));
+
+ // but becomes date/time
+ wrapper = new VisitorWrapper(null, "", new HashMap());
+ MAPPER.writer().without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .acceptJsonFormatVisitor(ZonedDateTime.class, wrapper);
+ properties = wrapper.getTraversedProperties();
+ _verifyDateTimeType(properties.get(""));
+ }
+
+ private void _verifyIntArrayType(String desc) {
+ assertEquals("ARRAY/Ljava/util/List;", desc);
+ }
+
+ private void _verifyTimeType(String desc) {
+ assertEquals("STRING/TIME", desc);
+ }
+
+ private void _verifyDateType(String desc) {
+ assertEquals("STRING/DATE", desc);
+ }
+
+ private void _verifyDateTimeType(String desc) {
+ assertEquals("STRING/DATE_TIME", desc);
+ }
+
+ private void _verifyBigDecimalType(String desc) {
+ assertEquals("NUMBER/BIG_DECIMAL", desc);
+ }
+
+ private void _verifyLongType(String desc) {
+ assertEquals("INTEGER/LONG", desc);
+ }
+
+ private void _verifyLongFormat(String desc) {
+ assertEquals("INTEGER/UTC_MILLISEC", desc);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java
new file mode 100644
index 0000000000..6e4a697105
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/DeductionTypeSerialization296Test.java
@@ -0,0 +1,86 @@
+package tools.jackson.databind.ext.javatime.misc;
+
+import java.time.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// for [modules-java8#296]: problem with `JsonTypeInfo.Id.DEDUCTION`
+public class DeductionTypeSerialization296Test extends DateTimeTestBase
+{
+ static class Wrapper {
+ @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
+ public Object value;
+
+ public Wrapper(Object value) {
+ this.value = value;
+ }
+ }
+
+ private final ObjectMapper MAPPER = mapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+
+ @Test
+ public void testLocalDate() throws Exception
+ {
+ LocalDate date = LocalDate.of(1986, Month.JANUARY, 17);
+ assertEquals(a2q("{'value':'1986-01-17'}"),
+ MAPPER.writeValueAsString(new Wrapper(date)));
+ }
+
+ @Test
+ public void testLocalDateTime() throws Exception
+ {
+ LocalDateTime datetime = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57);
+ assertEquals(a2q("{'value':'2013-08-21T09:22:00.000000057'}"),
+ MAPPER.writeValueAsString(new Wrapper(datetime)));
+ }
+
+ @Test
+ public void testLocalTime() throws Exception
+ {
+ LocalTime time = LocalTime.of(9, 22, 57);
+ assertEquals(a2q("{'value':'09:22:57'}"),
+ MAPPER.writeValueAsString(new Wrapper(time)));
+ }
+
+ @Test
+ public void testMonthDate() throws Exception
+ {
+ MonthDay date = MonthDay.of(Month.JANUARY, 17);
+ assertEquals(a2q("{'value':'--01-17'}"),
+ MAPPER.writeValueAsString(new Wrapper(date)));
+ }
+
+ @Test
+ public void testOffsetTime() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300"));
+ assertEquals(a2q("{'value':'15:43+03:00'}"),
+ MAPPER.writeValueAsString(new Wrapper(time)));
+ }
+
+ @Test
+ public void testYearMonth() throws Exception
+ {
+ YearMonth date = YearMonth.of(1986, Month.JANUARY);
+ assertEquals(a2q("{'value':'1986-01'}"),
+ MAPPER.writeValueAsString(new Wrapper(date)));
+ }
+
+ @Test
+ public void testZoneId() throws Exception
+ {
+ ZoneId zone = ZoneId.of("America/Denver");
+ assertEquals(a2q("{'value':'America/Denver'}"),
+ MAPPER.writeValueAsString(new Wrapper(zone)));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java
new file mode 100644
index 0000000000..162ef8014c
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/JDKSerializabilityTest.java
@@ -0,0 +1,46 @@
+package tools.jackson.databind.ext.javatime.misc;
+
+import java.io.*;
+import java.time.Year;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class JDKSerializabilityTest extends DateTimeTestBase
+{
+ @Test
+ public void testJDKSerializability() throws Exception {
+ final Year input = Year.of(1986);
+ ObjectMapper mapper = newMapper();
+ String json1 = mapper.writeValueAsString(input);
+
+ // validate we can still use it to deserialize jackson objects
+ ObjectMapper thawedMapper = serializeAndDeserialize(mapper);
+ String json2 = thawedMapper.writeValueAsString(input);
+
+ assertEquals(json1, json2);
+
+ Year result = thawedMapper.readValue(json1, Year.class);
+ assertEquals(input, result);
+ }
+
+ private ObjectMapper serializeAndDeserialize(ObjectMapper mapper) throws Exception {
+ //verify serialization
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
+
+ outputStream.writeObject(mapper);
+ byte[] serializedBytes = byteArrayOutputStream.toByteArray();
+
+ //verify deserialization
+ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedBytes);
+ ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
+
+ Object deserializedObject = inputStream.readObject();
+ return (ObjectMapper) deserializedObject;
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java b/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java
new file mode 100644
index 0000000000..62364ec763
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/misc/UnsupportedTypesTest.java
@@ -0,0 +1,34 @@
+package tools.jackson.databind.ext.javatime.misc;
+
+import java.time.temporal.TemporalAdjuster;
+import java.time.temporal.TemporalAdjusters;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class UnsupportedTypesTest extends DateTimeTestBase
+{
+ // [modules-java8#207]
+ static class TAWrapper {
+ public TemporalAdjuster a;
+
+ public TAWrapper(TemporalAdjuster a) {
+ this.a = a;
+ }
+ }
+
+ // [modules-java#207]: should not fail on `TemporalAdjuster`
+ @Test
+ public void testTemporalAdjusterSerialization() throws Exception
+ {
+ ObjectMapper mapper = newMapper();
+
+ // Not 100% sure how this happens, actually; should fail on empty "POJO"?
+ assertEquals(a2q("{'a':{}}"),
+ mapper.writeValueAsString(new TAWrapper(TemporalAdjusters.firstDayOfMonth())));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java
new file mode 100644
index 0000000000..6aba0e70d0
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/DurationSerTest.java
@@ -0,0 +1,322 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Duration;
+import java.time.temporal.TemporalAmount;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectWriter;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DurationSerTest extends DateTimeTestBase
+{
+ private final ObjectWriter WRITER = newMapper().writer();
+
+ // [datetime#224]
+ static class MyDto224 {
+ @JsonFormat(pattern = "MINUTES"
+ // Work-around from issue:
+// , without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS
+ )
+ @JsonProperty("mins")
+ final Duration duration;
+
+ public MyDto224(Duration d) { duration = d; }
+
+ public Duration getDuration() { return duration; }
+ }
+
+ // [datetime#282]
+ static class Bean282 {
+ @JsonFormat(pattern = "SECONDS")
+ public Duration duration;
+
+ public Bean282(Duration d) { duration = d; }
+ }
+
+ @Test
+ public void testSerializationAsTimestampNanoseconds01() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(60L, 0);
+ String value = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(duration);
+ assertEquals("60"+NO_NANOSECS_SUFFIX, value);
+ }
+
+ @Test
+ public void testSerializationAsTimestampNanoseconds02() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+ String value = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(duration);
+ assertEquals("13498.000008374", value);
+ }
+
+ // [modules-java8#165]
+ @Test
+ public void testSerializationAsTimestampNanoseconds03() throws Exception
+ {
+ ObjectWriter w = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+
+ // 20-Oct-2020, tatu: Very weird, but "use nanoseconds" actually results
+ // in unit being seconds, with fractions (with nanosec precision)
+ String value = w.writeValueAsString(Duration.ofMillis(1L));
+ assertEquals("0.001000000", value);
+
+ value = w.writeValueAsString(Duration.ofMillis(-1L));
+ assertEquals("-0.001000000", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestampMilliseconds01() throws Exception
+ {
+ final ObjectWriter w = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+ String value = w.writeValueAsString(Duration.ofSeconds(45L, 0));
+ assertEquals("45000", value);
+
+ // and with negative value too
+ value = w.writeValueAsString(Duration.ofSeconds(-32L, 0));
+ assertEquals("-32000", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestampMilliseconds02() throws Exception
+ {
+ String value = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(Duration.ofSeconds(13498L, 8374));
+ assertEquals("13498000", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestampMilliseconds03() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(13498L, 837481723);
+ String value = WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(duration);
+ assertEquals("13498837", value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(60L, 0);
+ String value = WRITER
+ .without(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .writeValueAsString(duration);
+ assertEquals(q(duration.toString()), value);
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+ String value = WRITER
+ .without(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .writeValueAsString(duration);
+ assertEquals(q(duration.toString()), value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS,
+ SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .build();
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+ String value = mapper.writeValueAsString(duration);
+
+ assertEquals("[\"" + Duration.class.getName() + "\",13498.000008374]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .build();
+ Duration duration = Duration.ofSeconds(13498L, 837481723);
+ String value = mapper.writeValueAsString(duration);
+
+ assertEquals("[\"" + Duration.class.getName() + "\",13498837]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .build();
+ Duration duration = Duration.ofSeconds(13498L, 8374);
+ String value = mapper.writeValueAsString(duration);
+
+ assertEquals("[\"" + Duration.class.getName() + "\",\"" + duration.toString() + "\"]", value);
+ }
+
+ /*
+ /**********************************************************
+ /* Tests for custom patterns (modules-java8#189)
+ /**********************************************************
+ */
+
+ @Test
+ public void shouldSerializeInNanos_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("NANOS");
+ assertEquals("3600000000000", mapper.writeValueAsString(Duration.ofHours(1)));
+ }
+
+ @Test
+ public void shouldSerializeInMicros_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MICROS");
+ assertEquals("1000", mapper.writeValueAsString(Duration.ofMillis(1)));
+ }
+
+ @Test
+ public void shouldSerializeInMicrosDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MICROS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofNanos(1500)));
+ }
+
+ @Test
+ public void shouldSerializeInMillis_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MILLIS");
+ assertEquals("1000", mapper.writeValueAsString(Duration.ofSeconds(1)));
+ }
+
+ @Test
+ public void shouldSerializeInMillisDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MILLIS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofNanos(1500000)));
+ }
+
+ @Test
+ public void shouldSerializeInSeconds_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("SECONDS");
+ assertEquals("60", mapper.writeValueAsString(Duration.ofMinutes(1)));
+ }
+
+ @Test
+ public void shouldSerializeInSecondsDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("SECONDS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofMillis(1500)));
+ }
+
+ @Test
+ public void shouldSerializeInMinutes_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
+ assertEquals("60", mapper.writeValueAsString(Duration.ofHours(1)));
+ }
+
+ @Test
+ public void shouldSerializeInMinutesDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("MINUTES");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofSeconds(90)));
+ }
+
+ @Test
+ public void shouldSerializeInHours_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("HOURS");
+ assertEquals("24", mapper.writeValueAsString(Duration.ofDays(1)));
+ }
+
+ @Test
+ public void shouldSerializeInHoursDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("HOURS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofMinutes(90)));
+ }
+
+ @Test
+ public void shouldSerializeInHalfDays_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("HALF_DAYS");
+ assertEquals("2", mapper.writeValueAsString(Duration.ofDays(1)));
+ }
+
+ @Test
+ public void shouldSerializeInHalfDaysDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("DAYS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofHours(30)));
+ }
+
+ @Test
+ public void shouldSerializeInDays_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("DAYS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofDays(1)));
+ }
+
+ @Test
+ public void shouldSerializeInDaysDiscardingFractions_whenSetAsPattern() throws Exception
+ {
+ ObjectMapper mapper = _mapperForPatternOverride("DAYS");
+ assertEquals("1", mapper.writeValueAsString(Duration.ofHours(36)));
+ }
+
+ protected ObjectMapper _mapperForPatternOverride(String pattern) {
+ ObjectMapper mapper = mapperBuilder()
+ .withConfigOverride(Duration.class,
+ cfg -> cfg.setFormat(JsonFormat.Value.forPattern(pattern)))
+ .enable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .build();
+ return mapper;
+ }
+
+ // [datetime#224]
+ @Test
+ public void testDurationFormatOverrideMinutes() throws Exception
+ {
+ assertEquals(a2q("{'mins':120}"),
+ WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .writeValueAsString(new MyDto224(Duration.ofHours(2))));
+ }
+
+ // [datetime#282]
+ @Test
+ public void testDurationFormatOverrideSeconds() throws Exception
+ {
+ final Duration maxDuration = Duration.ofSeconds(Long.MIN_VALUE);
+ assertEquals(a2q("{'duration':"+Long.MIN_VALUE+"}"),
+ WRITER
+ .with(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .writeValueAsString(new Bean282(maxDuration)));
+ }
+
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java
new file mode 100644
index 0000000000..ba80bb7eb3
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/InstantSerTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class InstantSerTest extends DateTimeTestBase
+{
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT;
+
+ private final ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerializationAsTimestamp01Nanoseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+
+ assertNotNull(value);
+ assertEquals(NO_NANOSECS_SER, value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp01Milliseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("0", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02Nanoseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("123456789.183917322", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02Milliseconds() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("123456789183", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ Instant date = Instant.now();
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals(DecimalUtils.toDecimal(date.getEpochSecond(), date.getNano()), value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Milliseconds() throws Exception
+ {
+ Instant date = Instant.now();
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals(Long.toString(date.toEpochMilli()), value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(0L);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString03() throws Exception
+ {
+ Instant date = Instant.now();
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String value = m.writeValueAsString(date);
+ assertEquals("[\"" + Instant.class.getName() + "\",123456789.183917322]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ Instant date = Instant.ofEpochSecond(123456789L, 183917322);
+ ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String value = m.writeValueAsString(date);
+ assertEquals("[\"" + Instant.class.getName() + "\",123456789183]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ Instant date = Instant.now();
+ ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String value = m.writeValueAsString(date);
+ assertEquals("[\"" + Instant.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", value);
+ }
+
+ static class Pojo1 {
+ @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
+ public Instant t1 = Instant.parse("2022-04-27T12:00:00Z");
+ public Instant t2 = t1;
+ }
+
+ @Test
+ public void testShapeInt() throws Exception {
+ String json1 = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(new Pojo1());
+ assertEquals("{\"t1\":1651060800000,\"t2\":1651060800.000000000}", json1);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java
new file mode 100644
index 0000000000..642611fa6e
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateSerTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateSerTest
+ extends DateTimeTestBase
+{
+ final static class EpochDayWrapper {
+ @JsonFormat(shape=JsonFormat.Shape.NUMBER_INT)
+ public LocalDate value;
+
+ public EpochDayWrapper() { }
+ public EpochDayWrapper(LocalDate v) { value = v; }
+ }
+
+ static class VanillaWrapper {
+ public LocalDate value;
+
+ public VanillaWrapper() { }
+ public VanillaWrapper(LocalDate v) { value = v; }
+ }
+
+ // [modules-java8#46]
+ static class Holder46 {
+ public LocalDate localDate;
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_OBJECT)
+ public Object object;
+
+ public Holder46(LocalDate localDate, Object object) {
+ this.localDate = localDate;
+ this.object = object;
+ }
+ }
+
+ private final ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerializationAsTimestamp01() throws Exception
+ {
+ LocalDate date = LocalDate.of(1986, Month.JANUARY, 17);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+
+ assertNotNull(value);
+ assertEquals("[1986,1,17]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02() throws Exception
+ {
+ LocalDate date = LocalDate.of(2013, Month.AUGUST, 21);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+
+ assertNotNull(value);
+ assertEquals("[2013,8,21]", value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ LocalDate date = LocalDate.of(1986, Month.JANUARY, 17);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+
+ assertNotNull(value);
+ assertEquals('"' + date.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ LocalDate date = LocalDate.of(2013, Month.AUGUST, 21);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertNotNull(value);
+ assertEquals('"' + date.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5);
+ String value = mapper.writeValueAsString(date);
+
+ assertNotNull(value);
+ assertEquals("[\"" + LocalDate.class.getName() + "\",\"" + date.toString() + "\"]", value);
+ }
+
+ // [modules-java8#46]
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ final LocalDate localDate = LocalDate.of(2017, 12, 5);
+ String json = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(new Holder46(localDate, localDate));
+ assertEquals(a2q("{\"localDate\":[2017,12,5],\"object\":{\"java.time.LocalDate\":[2017,12,5]}}"),
+ json);
+ }
+
+ @Test
+ public void testConfigOverrides() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ o -> o.setFormat(JsonFormat.Value.forPattern("yyyy_MM_dd")))
+ .build();
+ LocalDate date = LocalDate.of(2005, Month.NOVEMBER, 5);
+ VanillaWrapper input = new VanillaWrapper(date);
+ final String EXP_DATE = "\"2005_11_05\"";
+ String json = mapper.writeValueAsString(input);
+ assertEquals("{\"value\":"+EXP_DATE+"}", json);
+ assertEquals(EXP_DATE, mapper.writeValueAsString(date));
+
+ // and read back, too
+ VanillaWrapper output = mapper.readValue(json, VanillaWrapper.class);
+ assertEquals(input.value, output.value);
+ LocalDate date2 = mapper.readValue(EXP_DATE, LocalDate.class);
+ assertEquals(date, date2);
+ }
+
+ @Test
+ public void testConfigOverridesToEpochDay() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .withConfigOverride(LocalDate.class,
+ o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.NUMBER_INT)))
+ .build();
+ LocalDate date = LocalDate.ofEpochDay(1000);
+ VanillaWrapper input = new VanillaWrapper(date);
+ final String EXP_DATE = "1000";
+ String json = mapper.writeValueAsString(input);
+ assertEquals("{\"value\":"+EXP_DATE+"}", json);
+ assertEquals(EXP_DATE, mapper.writeValueAsString(date));
+
+ // and read back, too
+ VanillaWrapper output = mapper.readValue(json, VanillaWrapper.class);
+ assertEquals(input.value, output.value);
+ LocalDate date2 = mapper.readValue(EXP_DATE, LocalDate.class);
+ assertEquals(date, date2);
+ }
+
+ @Test
+ public void testCustomFormatToEpochDay() throws Exception
+ {
+ EpochDayWrapper w = MAPPER.readValue("{\"value\": 1000}", EpochDayWrapper.class);
+ LocalDate date = w.value;
+ assertNotNull(date);
+ assertEquals(LocalDate.ofEpochDay(1000), date);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java
new file mode 100644
index 0000000000..e3a84d2a1c
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalDateTimeSerTest
+ extends DateTimeTestBase
+{
+ static class LDTWrapper {
+ @JsonFormat(pattern="yyyy-MM-dd'A'HH:mm:ss")
+ public LocalDateTime value;
+
+ public LDTWrapper(LocalDateTime v) { value = v; }
+ }
+
+ // 05-Feb-2025, tatu: Use Jackson 2.x defaults wrt as-timestamps
+ // serialization
+ private final static ObjectMapper MAPPER = mapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+
+ @Test
+ public void testSerializationAsTimestamp01() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43);
+ assertEquals("[1986,1,17,15,43]",
+ MAPPER.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57);
+ String value = MAPPER.writeValueAsString(time);
+
+ assertEquals("[2013,8,21,9,22,57]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Nanosecond() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57);
+
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[2013,8,21,9,22,0,57]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Millisecond() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 0, 57);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[2013,8,21,9,22,0,0]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Nanosecond() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[2005,11,5,22,31,5,829837]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Millisecond() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422829837);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[2005,11,5,22,31,5,422]", value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(1986, Month.JANUARY, 17, 15, 43, 05);
+ final ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ assertEquals("\"1986-01-17T15:43:05\"", m.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57);
+
+ final ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ String value = m.writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString03() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ final ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ String value = m.writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationWithFormatOverride() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 999000);
+ assertEquals(a2q("{'value':'2005-11-05A22:31:05'}"),
+ MAPPER.writeValueAsString(new LDTWrapper(time)));
+
+ ObjectMapper m = mapperBuilder().withConfigOverride(LocalDateTime.class,
+ cfg -> cfg.setFormat(JsonFormat.Value.forPattern("yyyy-MM-dd'X'HH:mm")))
+ .build();
+ assertEquals(a2q("'2005-11-05X22:31'"), m.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+
+ final ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String value = m.writeValueAsString(time);
+ assertEquals("[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,829837]]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ final ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 422829837);
+ String value = m.writeValueAsString(time);
+ assertEquals("[\"" + LocalDateTime.class.getName() + "\",[2005,11,5,22,31,5,422]]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ final ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
+ String value = m.writeValueAsString(time);
+ assertEquals("[\"" + LocalDateTime.class.getName() + "\",\"" + time.toString() + "\"]", value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java
new file mode 100644
index 0000000000..c51f498780
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalTimeSerTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectWriter;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.annotation.JsonSerialize;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class LocalTimeSerTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+ private final ObjectWriter writer = MAPPER.writer();
+
+ // [modules-java8#115]
+ static class CustomLocalTimeSerializer extends LocalTimeSerializer {
+ public CustomLocalTimeSerializer() {
+ // Default doesn't cut it for us.
+ super(DateTimeFormatter.ofPattern("HH/mm"));
+ }
+ }
+
+ static class CustomWrapper {
+ @JsonSerialize(using = CustomLocalTimeSerializer.class)
+ public LocalTime value;
+
+ public CustomWrapper(LocalTime v) { value = v; }
+ }
+
+ @Test
+ public void testSerializationAsTimestamp01() throws Exception
+ {
+ String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(LocalTime.of(15, 43));
+ assertEquals("[15,43]", json, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02() throws Exception
+ {
+ String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(LocalTime.of(9, 22, 57));
+ assertEquals("[9,22,57]", json, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ String json = writer.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
+ SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(LocalTime.of(9, 22, 0, 57));
+ assertEquals("[9,22,0,57]", json, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Milliseconds() throws Exception
+ {
+ LocalTime time = LocalTime.of(9, 22, 0, 57);
+ ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .build();
+ String value = mapper.writeValueAsString(time);
+
+ assertEquals("[9,22,0,0]", value, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Nanoseconds() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true)
+ .build();
+ String value = mapper.writeValueAsString(time);
+ assertEquals("[22,31,5,829837]", value, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Milliseconds() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 422829837);
+ ObjectMapper mapper = newMapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .build();
+ String value = mapper.writeValueAsString(time);
+ assertEquals("[22,31,5,422]", value, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ LocalTime time = LocalTime.of(15, 43, 20);
+ ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .build();
+ assertEquals("\"15:43:20\"", mapper.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ LocalTime time = LocalTime.of(9, 22, 57);
+ ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .build();
+ String value = mapper.writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationAsString03() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper m = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .build();
+ String value = m.writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value, "The value is not correct.");
+ }
+
+ // [modules-java8#115]
+ @Test
+ public void testWithCustomSerializer() throws Exception
+ {
+ String json = MAPPER.writeValueAsString(new CustomWrapper(LocalTime.of(15, 43)));
+ assertEquals("{\"value\":\"15/43\"}", json, "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper m = newMapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String json = m.writeValueAsString(time);
+
+ assertEquals("[\"" + LocalTime.class.getName() + "\",[22,31,5,829837]]", json,
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 422829837);
+
+ ObjectMapper m = newMapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String json = m.writeValueAsString(time);
+ assertEquals("[\"" + LocalTime.class.getName() + "\",[22,31,5,422]]", json,
+ "The value is not correct.");
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ LocalTime time = LocalTime.of(22, 31, 5, 829837);
+ ObjectMapper m = newMapperBuilder()
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ String value = m.writeValueAsString(time);
+
+ assertEquals("[\"" + LocalTime.class.getName() + "\",\"" + time.toString() + "\"]", value,
+ "The value is not correct.");
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java
new file mode 100644
index 0000000000..7a9053936a
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/MonthDaySerTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Month;
+import java.time.MonthDay;
+import java.time.temporal.TemporalAccessor;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class MonthDaySerTest
+ extends DateTimeTestBase
+{
+ private ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerialization01() throws Exception
+ {
+ assertEquals("\"--01-17\"",
+ MAPPER.writeValueAsString(MonthDay.of(Month.JANUARY, 17)));
+ }
+
+ @Test
+ public void testSerialization02() throws Exception
+ {
+ assertEquals("\"--08-21\"",
+ MAPPER.writeValueAsString(MonthDay.of(Month.AUGUST, 21)));
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ final ObjectMapper mapper = mapperBuilder()
+ .addMixIn(TemporalAccessor.class, MockObjectConfiguration.class)
+ .build();
+ MonthDay monthDay = MonthDay.of(Month.NOVEMBER, 5);
+ String value = mapper.writeValueAsString(monthDay);
+ assertEquals("[\"" + MonthDay.class.getName() + "\",\"--11-05\"]", value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java
new file mode 100644
index 0000000000..f644feea05
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerTest.java
@@ -0,0 +1,290 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.ext.javatime.util.DecimalUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OffsetDateTimeSerTest
+ extends DateTimeTestBase
+{
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+ private static final ZoneId Z1 = ZoneId.of("America/Chicago");
+
+ private static final ZoneId Z2 = ZoneId.of("America/Anchorage");
+
+ private static final ZoneId Z3 = ZoneId.of("America/Los_Angeles");
+
+ static class Wrapper {
+ @JsonFormat(
+ pattern="yyyy_MM_dd'T'HH:mm:ssZ",
+ shape=JsonFormat.Shape.STRING)
+ public OffsetDateTime value;
+
+ public Wrapper() { }
+ public Wrapper(OffsetDateTime v) { value = v; }
+ }
+
+ private ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerializationAsTimestamp01Nanoseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("0.0", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp01Milliseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("0", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02Nanoseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("123456789.183917322", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02Milliseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals("123456789183", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals(DecimalUtils.toDecimal(date.toEpochSecond(), date.getNano()), value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Milliseconds() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(date);
+ assertEquals(Long.toString(date.toInstant().toEpochMilli()), value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"'
+ + FORMATTER.withZone(Z1).format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"'
+ + FORMATTER.withZone(Z2).format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString03() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"'
+ + FORMATTER.withZone(Z3).format(date) + '"', value);
+ }
+
+ // [modules-java#254]
+ @Test
+ public void testSerializationWithJsonFormat() throws Exception
+ {
+ OffsetDateTime t1 = OffsetDateTime.parse("2022-04-27T12:00:00+02:00");
+ Wrapper input = new Wrapper(t1);
+
+ // pattern="yyyy_MM_dd'T'HH:mm:ssZ"
+ assertEquals(a2q("{'value':'2022_04_27T12:00:00+0200'}"),
+ MAPPER.writeValueAsString(input));
+
+ ObjectMapper m = mapperBuilder().withConfigOverride(OffsetDateTime.class,
+ cfg -> cfg.setFormat(JsonFormat.Value.forPattern("yyyy.MM.dd'x'HH:mm:ss")))
+ .build();
+ assertEquals(a2q("'2022.04.27x12:00:00'"), m.writeValueAsString(t1));
+ }
+
+ @Test
+ public void testSerializationAsStringWithMapperTimeZone01() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
+ String value = newMapper()
+ .writer()
+ .with(TimeZone.getTimeZone(Z1))
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsStringWithMapperTimeZone02() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = newMapper()
+ .writer()
+ .with(TimeZone.getTimeZone(Z2))
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsStringWithMapperTimeZone03() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = newMapper()
+ .writer()
+ .with(TimeZone.getTimeZone(Z3))
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+ assertEquals('"' + FORMATTER.format(date) + '"', value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = newMapperBuilder()
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
+ SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build()
+ .writeValueAsString(date);
+ assertEquals("[\"" + OffsetDateTime.class.getName() + "\",123456789.183917322]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
+ String value = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .build()
+ .writeValueAsString(date);
+ assertEquals("[\"" + OffsetDateTime.class.getName() + "\",123456789183]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ ObjectMapper m = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .build();
+ String value = m.writeValueAsString(date);
+ assertEquals("[\"" + OffsetDateTime.class.getName() + "\",\""
+ + FORMATTER.withZone(Z3).format(date) + "\"]", value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfoAndMapperTimeZone() throws Exception
+ {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = newMapperBuilder()
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build()
+ .writer()
+ .with(TimeZone.getTimeZone(Z3))
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(date);
+
+ assertEquals("[\"" + OffsetDateTime.class.getName() + "\",\"" + FORMATTER.format(date) + "\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOn() throws Exception {
+ OffsetDateTime date = OffsetDateTime.now(Z3);
+ String value = MAPPER.writer()
+ .with(TimeZone.getTimeZone(Z2))
+ .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID)
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE)
+ .writeValueAsString(date);
+
+ // We expect to have the date written with the ZoneId Z2
+ assertEquals("\"" + FORMATTER.format(date.atZoneSameInstant(Z2)) + "\"", value);
+ }
+
+ @Test
+ public void testSerializationAsStringWithDefaultTimeZoneAndContextTimeZoneOff() throws Exception {
+ ZonedDateTime date = ZonedDateTime.now(Z3);
+ String value = MAPPER.writer()
+ .with(TimeZone.getTimeZone(Z2))
+ .without(SerializationFeature.WRITE_DATES_WITH_ZONE_ID)
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE)
+ .writeValueAsString(date);
+
+ // We expect to have the date written with the ZoneId Z3
+ assertEquals("\"" + FORMATTER.format(date) + "\"", value);
+ }
+
+ static class Pojo1 {
+ @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
+ public OffsetDateTime t1 = OffsetDateTime.parse("2022-04-27T12:00:00+02:00");
+ public OffsetDateTime t2 = t1;
+ }
+
+ @Test
+ public void testShapeInt() throws Exception {
+ String json1 = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(new Pojo1());
+ assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java
new file mode 100644
index 0000000000..5635fc36d9
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OffsetTimeSerTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.time.temporal.Temporal;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.*;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OffsetTimeSerTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerializationAsTimestamp01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(time);
+ assertEquals("[15,43,\"+03:00\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(time);
+ assertEquals("[9,22,57,\"-06:30\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Nanoseconds() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[9,22,0,57,\"-06:30\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp03Milliseconds() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 0, 57, ZoneOffset.of("-0630"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[9,22,0,0,\"-06:30\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Nanoseconds() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .with(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[22,31,5,829837,\"+11:00\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsTimestamp04Milliseconds() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 422829837, ZoneOffset.of("+1100"));
+ String value = MAPPER.writer()
+ .with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .without(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
+ .writeValueAsString(time);
+ assertEquals("[22,31,5,422,\"+11:00\"]", value);
+ }
+
+ @Test
+ public void testSerializationAsString01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(15, 43, 0, 0, ZoneOffset.of("+0300"));
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(9, 22, 57, 0, ZoneOffset.of("-0630"));
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationAsString03() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+ String value = MAPPER.writer()
+ .without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .writeValueAsString(time);
+ assertEquals('"' + time.toString() + '"', value);
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, true)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+ assertEquals("[\"" + OffsetTime.class.getName() + "\",[22,31,5,829837,\"+11:00\"]]",
+ mapper.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo02() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 422829837, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)
+ .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+
+ assertEquals("[\"" + OffsetTime.class.getName() + "\",[22,31,5,422,\"+11:00\"]]",
+ mapper.writeValueAsString(time));
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo03() throws Exception
+ {
+ OffsetTime time = OffsetTime.of(22, 31, 5, 829837, ZoneOffset.of("+1100"));
+
+ final ObjectMapper mapper = newMapperBuilder()
+ .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+ .addMixIn(Temporal.class, MockObjectConfiguration.class)
+ .build();
+
+ assertEquals("[\"" + OffsetTime.class.getName() + "\",\"" + time.toString() + "\"]",
+ mapper.writeValueAsString(time));
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java
new file mode 100644
index 0000000000..8cb58a109b
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/OneBasedMonthSerTest.java
@@ -0,0 +1,64 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Month;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectWriter;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.json.JsonMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OneBasedMonthSerTest extends DateTimeTestBase
+{
+ static class Wrapper {
+ public Month month;
+
+ public Wrapper(Month m) { month = m; }
+ public Wrapper() { }
+ }
+
+ @Test
+ public void testSerializationFromEnum() throws Exception
+ {
+ assertEquals( "\"JANUARY\"" , writerForOneBased()
+ .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
+ .writeValueAsString(Month.JANUARY));
+ assertEquals( "\"JANUARY\"" , writerForZeroBased()
+ .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
+ .writeValueAsString(Month.JANUARY));
+ }
+
+ @Test
+ public void testSerializationFromEnumWithPattern_oneBased() throws Exception
+ {
+ ObjectWriter w = writerForOneBased().with(SerializationFeature.WRITE_ENUMS_USING_INDEX);
+ assertEquals( "{\"month\":1}" , w.writeValueAsString(new Wrapper(Month.JANUARY)));
+ }
+
+ @Test
+ public void testSerializationFromEnumWithPattern_zeroBased() throws Exception
+ {
+ ObjectWriter w = writerForZeroBased().with(SerializationFeature.WRITE_ENUMS_USING_INDEX);
+ assertEquals( "{\"month\":0}" , w.writeValueAsString(new Wrapper(Month.JANUARY)));
+ }
+
+
+ private ObjectWriter writerForZeroBased() {
+ return JsonMapper.builder()
+ .disable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build()
+ .writer();
+ }
+
+ private ObjectWriter writerForOneBased() {
+ return JsonMapper.builder()
+ .enable(DateTimeFeature.ONE_BASED_MONTHS)
+ .build()
+ .writer();
+ }
+
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java
new file mode 100644
index 0000000000..0ff889e53d
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/PeriodSerTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 FasterXML.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.Period;
+import java.time.temporal.TemporalAmount;
+
+import org.junit.jupiter.api.Test;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.DateTimeTestBase;
+import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class PeriodSerTest extends DateTimeTestBase
+{
+ private final ObjectMapper MAPPER = newMapper();
+
+ @Test
+ public void testSerialization01() throws Exception
+ {
+ assertEquals(q("P1Y6M15D"), MAPPER.writeValueAsString(Period.of(1, 6, 15)));
+ }
+
+ @Test
+ public void testSerialization02() throws Exception
+ {
+ assertEquals(q("P21D"), MAPPER.writeValueAsString(Period.of(0, 0, 21)));
+ }
+
+ @Test
+ public void testSerializationWithTypeInfo01() throws Exception
+ {
+ Period period = Period.of(5, 1, 12);
+ final ObjectMapper mapper = mapperBuilder()
+ .addMixIn(TemporalAmount.class, MockObjectConfiguration.class)
+ .build();
+ String value = mapper.writeValueAsString(period);
+ assertEquals("[" + q(Period.class.getName()) + ",\"P5Y1M12D\"]", value);
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java
new file mode 100644
index 0000000000..e0bd2f81df
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateSerializationWithCustomFormatter.java
@@ -0,0 +1,64 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import tools.jackson.core.json.JsonWriteFeature;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.deser.LocalDateDeserializer;
+import tools.jackson.databind.ext.javatime.ser.LocalDateSerializer;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestLocalDateSerializationWithCustomFormatter
+{
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testSerialization(DateTimeFormatter formatter) throws Exception {
+ LocalDate date = LocalDate.now();
+ assertTrue(serializeWith(date, formatter).contains(date.format(formatter)),
+ "Serialized value should contain the formatted date");
+ }
+
+ private String serializeWith(LocalDate date, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES)
+ .addModule(new SimpleModule()
+ .addSerializer(new LocalDateSerializer(f)))
+ .build();
+ return mapper.writeValueAsString(date);
+ }
+
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testDeserialization(DateTimeFormatter formatter) throws Exception {
+ LocalDate date = LocalDate.now();
+ assertEquals(date, deserializeWith(date.format(formatter), formatter),
+ "Deserialized value should match the original date");
+ }
+
+ private LocalDate deserializeWith(String json, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new SimpleModule()
+ .addDeserializer(LocalDate.class, new LocalDateDeserializer(f)))
+ .build();
+ return mapper.readValue("\"" + json + "\"", LocalDate.class);
+ }
+
+ static Stream customFormatters() {
+ return Stream.of(
+ DateTimeFormatter.BASIC_ISO_DATE,
+ DateTimeFormatter.ISO_DATE,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ORDINAL_DATE,
+ DateTimeFormatter.ISO_WEEK_DATE,
+ DateTimeFormatter.ofPattern("MM/dd/yyyy")
+ );
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java
new file mode 100644
index 0000000000..d95bf6bb00
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalDateTimeSerializationWithCustomFormatter.java
@@ -0,0 +1,57 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer;
+import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestLocalDateTimeSerializationWithCustomFormatter
+{
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testSerialization(DateTimeFormatter formatter) throws Exception {
+ LocalDateTime dateTime = LocalDateTime.now();
+ assertTrue(serializeWith(dateTime, formatter).contains(dateTime.format(formatter)));
+ }
+
+ private String serializeWith(LocalDateTime dateTime, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new SimpleModule()
+ .addSerializer(new LocalDateTimeSerializer(f)))
+ .build();
+ return mapper.writeValueAsString(dateTime);
+ }
+
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testDeserialization(DateTimeFormatter formatter) throws Exception {
+ LocalDateTime dateTime = LocalDateTime.now();
+ assertEquals(dateTime, deserializeWith(dateTime.format(formatter), formatter));
+ }
+
+ private LocalDateTime deserializeWith(String json, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new SimpleModule()
+ .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(f)))
+ .build();
+ return mapper.readValue("\"" + json + "\"", LocalDateTime.class);
+ }
+
+ static Stream customFormatters() {
+ return Stream.of(
+ DateTimeFormatter.ISO_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME
+ );
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java
new file mode 100644
index 0000000000..f0c94b7899
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/TestLocalTimeSerializationWithCustomFormatter.java
@@ -0,0 +1,56 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.stream.Stream;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ext.javatime.deser.LocalTimeDeserializer;
+import tools.jackson.databind.ext.javatime.ser.LocalTimeSerializer;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestLocalTimeSerializationWithCustomFormatter
+{
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testSerialization(DateTimeFormatter formatter) throws Exception {
+ LocalTime dateTime = LocalTime.now();
+ assertTrue(serializeWith(dateTime, formatter).contains(dateTime.format(formatter)));
+ }
+
+ private String serializeWith(LocalTime dateTime, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new SimpleModule()
+ .addSerializer(new LocalTimeSerializer(f)))
+ .build();
+ return mapper.writeValueAsString(dateTime);
+ }
+
+ @ParameterizedTest
+ @MethodSource("customFormatters")
+ void testDeserialization(DateTimeFormatter formatter) throws Exception {
+ LocalTime dateTime = LocalTime.now();
+ assertEquals(dateTime, deserializeWith(dateTime.format(formatter), formatter));
+ }
+
+ private LocalTime deserializeWith(String json, DateTimeFormatter f) throws Exception {
+ ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new SimpleModule()
+ .addDeserializer(LocalTime.class, new LocalTimeDeserializer(f)))
+ .build();
+ return mapper.readValue("\"" + json + "\"", LocalTime.class);
+ }
+
+ static Stream