diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 11c5b3a518..e4caea5ace 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -608,6 +608,8 @@ seonwoo_jung (@seonwooj0810) * Fixed #6101: `@JsonInclude(NON_EMPTY, content=CUSTOM)` does not omit a Map property after all entries are filtered [3.2.2] + * Contributed #6151: Add `DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS` + [3.3.0] @doeiqts * Reported #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully diff --git a/release-notes/VERSION b/release-notes/VERSION index ce6c0be76d..d1015f51d5 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -38,6 +38,8 @@ Versions: 3.x (for earlier see VERSION-2.x) #6142: Invalidate read-only lookup snapshot in `SerializerCache` when a typed serializer entry is replaced (and not just when added) (fix by @Dongnyoung) +#6151: Add `DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS` + (contributed by @seonwooj0810) 3.2.2 (not yet released) diff --git a/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java index 083396f3f2..c80f94b198 100644 --- a/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java +++ b/src/main/java/tools/jackson/databind/cfg/DateTimeFeature.java @@ -49,6 +49,42 @@ public enum DateTimeFeature implements DatatypeFeature */ ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false), + /** + * Feature that determines whether sub-second digits are always written when + * serializing {@link java.time.Instant}, {@link java.time.OffsetDateTime}, + * {@link java.time.ZonedDateTime} and {@link java.time.LocalDateTime} as + * ISO-8601 Strings using the default format. + *
+ * When disabled (the default), the JDK-provided ISO formatters are used and + * a zero sub-second value is omitted altogether -- {@code 2017-09-14T04:28:48Z} + * -- which means that output width varies with the value, breaking systems + * that expect fixed-precision timestamps (or that sort timestamps as text). + *
+ * When enabled, at least 3 (millisecond) sub-second digits are always written, + * zero-padded if necessary -- {@code 2017-09-14T04:28:48.000Z}. Higher precision + * is preserved: up to 9 digits are written, as many as needed to avoid losing + * information (but with no trailing zeroes beyond the 3 digit minimum, so + * {@code 123400000} nanoseconds is written as {@code .1234}). + *
+ * Only affects the default format: an explicit {@code DateTimeFormatter} or + * a {@link com.fasterxml.jackson.annotation.JsonFormat} pattern takes precedence, + * as does writing values as numeric timestamps. + *
+ * NOTE: only applies to Date/Time values, and NOT to Date/Time values used as + * {@link java.util.Map} keys: keys are written by separate key serializers that + * are not affected by this setting. + *
+ * Note, too, that the very extremes of the {@link java.time.Instant} range + * (notably {@link java.time.Instant#MIN} and {@link java.time.Instant#MAX}, which + * fall outside the range of {@link java.time.LocalDate}) cannot be written with + * Date/Time fields at all, and retain default handling regardless of this setting. + *
+ * Default setting is disabled, for backwards compatibility.
+ *
+ * @since 3.3
+ */
+ ALWAYS_WRITE_SUBSECOND_DIGITS(false),
+
/**
* Feature that determines whether {@link java.time.ZoneId} is normalized
* (via call to {@code java.time.ZoneId#normalized()}) when deserializing
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
index 5b8b401b08..5cae773b40 100644
--- a/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/InstantSerializer.java
@@ -18,6 +18,7 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.Instant;
+import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@@ -31,6 +32,12 @@ public class InstantSerializer extends InstantSerializerBase
+ * Implementations MUST return {@code null} unless given {@code defaultFormat} is
+ * the standard built-in default of the subclass: some subclasses (notably
+ * {@link ZonedDateTimeSerializer}) allow caller-provided default formatters, and
+ * those must not be overridden by the feature. Implementations should also return
+ * {@code null} for values the replacement cannot express, so that default handling
+ * is retained instead of failing. Returning {@code null} (which the base
+ * implementation always does) means the feature has no effect.
+ *
+ * @param value Value being serialized
+ * @param defaultFormat Default formatter that would be used if the feature was
+ * not enabled (possibly {@code null})
+ *
+ * @since 3.3
+ */
+ protected DateTimeFormatter _alwaysWriteSubSecondDigitsFormatter(T value,
+ DateTimeFormatter defaultFormat) {
+ return null;
+ }
+
protected String formatValue(T value, SerializationContext ctxt)
{
DateTimeFormatter formatter = (_formatter == null) ? defaultFormat :_formatter;
+ if ((_formatter == null) && ctxt.isEnabled(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
+ DateTimeFormatter subSecondFormatter = _alwaysWriteSubSecondDigitsFormatter(value, defaultFormat);
+ if (subSecondFormatter != null) {
+ formatter = subSecondFormatter;
+ }
+ }
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
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
index 9c70236e3e..cbda52597e 100644
--- a/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerializer.java
@@ -76,11 +76,7 @@ public void serialize(LocalDateTime value, JsonGenerator g, SerializationContext
_serializeAsArrayContents(value, g, ctxt);
g.writeEndArray();
} else {
- DateTimeFormatter dtf = _formatter;
- if (dtf == null) {
- dtf = _defaultFormatter();
- }
- g.writeString(value.format(dtf));
+ g.writeString(value.format(_effectiveFormatter(ctxt)));
}
}
@@ -101,15 +97,34 @@ public void serializeWithType(LocalDateTime value, JsonGenerator g, Serializatio
&& typeIdDef.valueShape == JsonToken.START_ARRAY) {
_serializeAsArrayContents(value, g, ctxt);
} else {
- DateTimeFormatter dtf = _formatter;
- if (dtf == null) {
- dtf = _defaultFormatter();
- }
- g.writeString(value.format(dtf));
+ g.writeString(value.format(_effectiveFormatter(ctxt)));
}
typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
}
+ /**
+ * Resolves the formatter to use when no numeric-timestamp shape applies: the
+ * explicit per-property {@code _formatter} if set, else the default from
+ * {@link #_defaultFormatter()}, or -- if
+ * {@link DateTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS} is enabled -- a
+ * counterpart of the standard default that always writes sub-second digits.
+ *
+ * @since 3.3
+ */
+ private DateTimeFormatter _effectiveFormatter(SerializationContext ctxt) {
+ if (_formatter != null) {
+ return _formatter;
+ }
+ DateTimeFormatter dtf = _defaultFormatter();
+ // Sub-second replacement is a counterpart of the standard built-in default
+ // only: a subclass-provided default must not be overridden by the feature
+ if ((dtf == DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+ && ctxt.isEnabled(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
+ return SubSecondFormatters.LOCAL_DATE_TIME;
+ }
+ return dtf;
+ }
+
private final void _serializeAsArrayContents(LocalDateTime value, JsonGenerator g,
SerializationContext ctxt)
throws JacksonException
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
index bd47ae26cb..b7825b5bc3 100644
--- a/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/OffsetDateTimeSerializer.java
@@ -61,4 +61,11 @@ protected JSR310FormattedSerializerBase> withFeatures(Boolean writeZoneId, Boo
return new OffsetDateTimeSerializer(this, _formatter,
_useTimestamp, writeNanoseconds, _shape);
}
+
+ @Override
+ protected DateTimeFormatter _alwaysWriteSubSecondDigitsFormatter(OffsetDateTime value,
+ DateTimeFormatter defaultFormat) {
+ return (defaultFormat == DateTimeFormatter.ISO_OFFSET_DATE_TIME)
+ ? SubSecondFormatters.OFFSET_DATE_TIME : null;
+ }
}
diff --git a/src/main/java/tools/jackson/databind/ext/javatime/ser/SubSecondFormatters.java b/src/main/java/tools/jackson/databind/ext/javatime/ser/SubSecondFormatters.java
new file mode 100644
index 0000000000..4b88ab2e92
--- /dev/null
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/SubSecondFormatters.java
@@ -0,0 +1,74 @@
+package tools.jackson.databind.ext.javatime.ser;
+
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
+
+/**
+ * Container for the ISO-8601 {@link DateTimeFormatter}s used in place of the
+ * JDK-provided defaults when
+ * {@link tools.jackson.databind.cfg.DateTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}
+ * is enabled.
+ *
+ * These differ from the JDK counterparts only in the sub-second field: instead of
+ * omitting it when zero, at least 3 (millisecond) digits are always written, and up
+ * to 9 when the value carries higher precision (so nothing is truncated).
+ *
+ * @since 3.3
+ */
+final class SubSecondFormatters
+{
+ private SubSecondFormatters() { }
+
+ /**
+ * Date and time down to the seconds field, followed by 3 to 9 sub-second digits:
+ * the shared prefix of all formatters here.
+ */
+ private static DateTimeFormatterBuilder _localDateTimeBuilder() {
+ return new DateTimeFormatterBuilder()
+ .append(DateTimeFormatter.ISO_LOCAL_DATE)
+ .appendLiteral('T')
+ .appendValue(ChronoField.HOUR_OF_DAY, 2)
+ .appendLiteral(':')
+ .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ .appendLiteral(':')
+ .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+ .appendFraction(ChronoField.NANO_OF_SECOND, 3, 9, true);
+ }
+
+ /**
+ * Counterpart of {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}.
+ */
+ static final DateTimeFormatter LOCAL_DATE_TIME = _localDateTimeBuilder()
+ .toFormatter();
+
+ /**
+ * Counterpart of {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}.
+ */
+ static final DateTimeFormatter OFFSET_DATE_TIME = _localDateTimeBuilder()
+ .appendOffsetId()
+ .toFormatter();
+
+ /**
+ * Counterpart of {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}, that is,
+ * {@link #OFFSET_DATE_TIME} with the optional {@code [Zone/Id]} suffix.
+ */
+ static final DateTimeFormatter ZONED_DATE_TIME = new DateTimeFormatterBuilder()
+ .append(OFFSET_DATE_TIME)
+ .optionalStart()
+ .appendLiteral('[')
+ .parseCaseSensitive()
+ .appendZoneRegionId()
+ .appendLiteral(']')
+ .toFormatter();
+
+ /**
+ * Counterpart of {@link DateTimeFormatter#ISO_INSTANT} (and of
+ * {@link java.time.Instant#toString()}, which is what the default
+ * {@code Instant} serialization actually uses): UTC-based, so the
+ * offset is always rendered as {@code Z}.
+ */
+ static final DateTimeFormatter INSTANT = OFFSET_DATE_TIME
+ .withZone(ZoneOffset.UTC);
+}
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
index 4641724a63..760699975b 100644
--- a/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java
+++ b/src/main/java/tools/jackson/databind/ext/javatime/ser/ZonedDateTimeSerializer.java
@@ -68,17 +68,21 @@ public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext
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)) {
+ // [modules-java8#333], [databind#6151]: explicitly configured format should
+ // override `DateTimeFeature.WRITE_DATES_WITH_ZONE_ID`
+ if (_hasExplicitFormat()) {
; // use default handling
} else if (shouldWriteWithZoneId(ctxt)) {
// Apply millisecond truncation if enabled
if (ctxt.isEnabled(DateTimeFeature.TRUNCATE_TO_MSECS_ON_WRITE)) {
value = value.truncatedTo(ChronoUnit.MILLIS);
}
- // write with zone
- g.writeString(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(value));
+ // write with zone (note: only standard default format gets here, so
+ // sub-second variant may be used as-is)
+ DateTimeFormatter formatter = ctxt.isEnabled(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ ? SubSecondFormatters.ZONED_DATE_TIME
+ : DateTimeFormatter.ISO_ZONED_DATE_TIME;
+ g.writeString(formatter.format(value));
return;
}
}
@@ -88,16 +92,35 @@ public void serialize(ZonedDateTime value, JsonGenerator g, SerializationContext
@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) {
+ // [modules-java8#333], [databind#6151]: when an explicitly configured format is
+ // used, Zone Id is only added if specifically requested (via `@JsonFormat`),
+ // and NOT due to `DateTimeFeature.WRITE_DATES_WITH_ZONE_ID`
+ if (_hasExplicitFormat()) {
// Why not `if (shouldWriteWithZoneId(provider))` ?
if (Boolean.TRUE.equals(_writeZoneId)) {
formatted += "[" + value.getZone().getId() + "]";
}
}
return formatted;
- }
+ }
+
+ /**
+ * Accessor for checking whether this serializer has an explicitly configured
+ * format that should be used as-is, taking precedence over
+ * {@link DateTimeFeature#WRITE_DATES_WITH_ZONE_ID}: either a {@code @JsonFormat}
+ * pattern (with String shape), or a caller-provided default formatter (see
+ * {@link #ZonedDateTimeSerializer(DateTimeFormatter)}).
+ *
+ * @since 3.3
+ */
+ protected boolean _hasExplicitFormat() {
+ if ((_formatter != null) && (_shape == JsonFormat.Shape.STRING)) {
+ return true;
+ }
+ DateTimeFormatter df = _defaultFormat();
+ return (df != null) && (df != DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
public boolean shouldWriteWithZoneId(SerializationContext ctxt) {
return (_writeZoneId != null)
? _writeZoneId
@@ -111,4 +134,13 @@ protected JsonToken serializationShape(SerializationContext ctxt) {
}
return super.serializationShape(ctxt);
}
+
+ @Override
+ protected DateTimeFormatter _alwaysWriteSubSecondDigitsFormatter(ZonedDateTime value,
+ DateTimeFormatter defaultFormat) {
+ // 10-Aug-2026, tatu: Caller may pass its own default formatter (see
+ // `ZonedDateTimeSerializer(DateTimeFormatter)`); if so, must not override it
+ return (defaultFormat == DateTimeFormatter.ISO_OFFSET_DATE_TIME)
+ ? SubSecondFormatters.OFFSET_DATE_TIME : null;
+ }
}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/AlwaysWriteSubSecondDigitsTest.java b/src/test/java/tools/jackson/databind/ext/javatime/AlwaysWriteSubSecondDigitsTest.java
new file mode 100644
index 0000000000..ae5d243afb
--- /dev/null
+++ b/src/test/java/tools/jackson/databind/ext/javatime/AlwaysWriteSubSecondDigitsTest.java
@@ -0,0 +1,224 @@
+package tools.jackson.databind.ext.javatime;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.ext.javatime.ser.ZonedDateTimeSerializer;
+import tools.jackson.databind.module.SimpleModule;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests for {@link DateTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}.
+ *
+ * Ported from the equivalent 2.x feature in
+ * {@code jackson-modules-java8} (see
+ * modules-java8#386,
+ * fixing modules-java8#76).
+ */
+public class AlwaysWriteSubSecondDigitsTest extends DateTimeTestBase
+{
+ static class Wrapper {
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
+ public OffsetDateTime value;
+
+ Wrapper(OffsetDateTime v) { value = v; }
+ }
+
+ private final ObjectMapper MAPPER = newMapperBuilder()
+ .enable(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ .build();
+
+ private final ObjectMapper DEFAULT_MAPPER = newMapper();
+
+ @Test
+ public void testInstantZeroSubSecond() throws Exception
+ {
+ Instant value = Instant.parse("2017-09-14T04:28:48Z");
+ // Default: sub-second field omitted entirely
+ assertEquals(q("2017-09-14T04:28:48Z"), DEFAULT_MAPPER.writeValueAsString(value));
+ // Enabled: zero-padded to millisecond precision
+ assertEquals(q("2017-09-14T04:28:48.000Z"), MAPPER.writeValueAsString(value));
+ }
+
+ @Test
+ public void testInstantHigherPrecisionNotTruncated() throws Exception
+ {
+ assertEquals(q("2017-09-14T04:28:48.100Z"),
+ MAPPER.writeValueAsString(Instant.parse("2017-09-14T04:28:48.100Z")));
+ assertEquals(q("2017-09-14T04:28:48.123456Z"),
+ MAPPER.writeValueAsString(Instant.parse("2017-09-14T04:28:48.123456Z")));
+ assertEquals(q("2017-09-14T04:28:48.123456789Z"),
+ MAPPER.writeValueAsString(Instant.parse("2017-09-14T04:28:48.123456789Z")));
+ }
+
+ @Test
+ public void testOffsetDateTime() throws Exception
+ {
+ OffsetDateTime value = OffsetDateTime.parse("2017-09-14T04:28:48+02:00");
+ assertEquals(q("2017-09-14T04:28:48+02:00"), DEFAULT_MAPPER.writeValueAsString(value));
+ assertEquals(q("2017-09-14T04:28:48.000+02:00"), MAPPER.writeValueAsString(value));
+
+ // Note: the JDK ISO formatter renders 100 msec as ".1"; with the feature on,
+ // width is stable at (at least) 3 digits
+ OffsetDateTime millis = OffsetDateTime.parse("2017-09-14T04:28:48.100+02:00");
+ assertEquals(q("2017-09-14T04:28:48.1+02:00"), DEFAULT_MAPPER.writeValueAsString(millis));
+ assertEquals(q("2017-09-14T04:28:48.100+02:00"), MAPPER.writeValueAsString(millis));
+ }
+
+ @Test
+ public void testZonedDateTime() throws Exception
+ {
+ ZonedDateTime value = ZonedDateTime.parse("2017-09-14T04:28:48+02:00[Europe/Budapest]");
+ assertEquals(q("2017-09-14T04:28:48.000+02:00"), MAPPER.writeValueAsString(value));
+ }
+
+ @Test
+ public void testZonedDateTimeWithZoneId() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .enable(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .build();
+ ZonedDateTime value = ZonedDateTime.parse("2017-09-14T04:28:48+02:00[Europe/Budapest]");
+ assertEquals(q("2017-09-14T04:28:48.000+02:00[Europe/Budapest]"),
+ mapper.writeValueAsString(value));
+
+ // Same path, feature disabled: zero sub-second is omitted, matching the JDK formatter
+ ObjectMapper defaultZoneIdMapper = newMapperBuilder()
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .build();
+ assertEquals(q("2017-09-14T04:28:48+02:00[Europe/Budapest]"),
+ defaultZoneIdMapper.writeValueAsString(value));
+ }
+
+ @Test
+ public void testLocalDateTime() throws Exception
+ {
+ LocalDateTime value = LocalDateTime.parse("2017-09-14T04:28:48");
+ assertEquals(q("2017-09-14T04:28:48"), DEFAULT_MAPPER.writeValueAsString(value));
+ assertEquals(q("2017-09-14T04:28:48.000"), MAPPER.writeValueAsString(value));
+
+ // Seconds keep being written even when zero (as with the JDK ISO formatter)
+ LocalDateTime noSeconds = LocalDateTime.parse("2017-09-14T04:28");
+ assertEquals(q("2017-09-14T04:28:00"), DEFAULT_MAPPER.writeValueAsString(noSeconds));
+ assertEquals(q("2017-09-14T04:28:00.000"), MAPPER.writeValueAsString(noSeconds));
+ }
+
+ // Feature must not leak into numeric timestamp serialization
+ @Test
+ public void testTimestampsUnaffected() throws Exception
+ {
+ ObjectMapper mapper = newMapperBuilder()
+ .enable(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ .enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+ assertEquals("1505363328.000000000",
+ mapper.writeValueAsString(Instant.parse("2017-09-14T04:28:48Z")));
+ }
+
+ // ... nor override an explicit `@JsonFormat` pattern
+ @Test
+ public void testExplicitPatternWins() throws Exception
+ {
+ assertEquals(a2q("{'value':'2017-09-14T04:28:48'}"),
+ MAPPER.writeValueAsString(new Wrapper(OffsetDateTime.parse("2017-09-14T04:28:48Z"))));
+ }
+
+ // ... nor a default formatter passed to the serializer by the caller
+ @Test
+ public void testCallerProvidedDefaultFormatterWins() throws Exception
+ {
+ DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy_MM_dd'X'HH:mm:ss");
+ ObjectMapper mapper = newMapperBuilder()
+ .addModule(new SimpleModule()
+ .addSerializer(new ZonedDateTimeSerializer(df)))
+ .enable(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ .build();
+ assertEquals(q("2017_09_14X04:28:48"), mapper.writeValueAsString(
+ ZonedDateTime.parse("2017-09-14T04:28:48+02:00[Europe/Budapest]")));
+ }
+
+ // ... including on the separate "write with Zone Id" path
+ @Test
+ public void testCallerProvidedDefaultFormatterWithZoneId() throws Exception
+ {
+ DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy_MM_dd'X'HH:mm:ss");
+ ZonedDateTime value = ZonedDateTime.parse("2017-09-14T04:28:48+02:00[Europe/Budapest]");
+ ObjectMapper mapper = newMapperBuilder()
+ .addModule(new SimpleModule()
+ .addSerializer(new ZonedDateTimeSerializer(df)))
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .enable(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)
+ .build();
+ ObjectMapper defaultMapper = newMapperBuilder()
+ .addModule(new SimpleModule()
+ .addSerializer(new ZonedDateTimeSerializer(df)))
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .build();
+ assertEquals(defaultMapper.writeValueAsString(value),
+ mapper.writeValueAsString(value));
+ assertEquals(q("2017_09_14X04:28:48"), mapper.writeValueAsString(value));
+ }
+
+ // Map keys are written by separate key serializers, not affected by the feature
+ @Test
+ public void testMapKeysUnaffected() throws Exception
+ {
+ assertEquals(a2q("{'2017-09-14T04:28:48Z':1}"),
+ MAPPER.writeValueAsString(
+ Collections.singletonMap(Instant.parse("2017-09-14T04:28:48Z"), 1)));
+ assertEquals(a2q("{'2017-09-14T04:28:48':1}"),
+ MAPPER.writeValueAsString(
+ Collections.singletonMap(LocalDateTime.parse("2017-09-14T04:28:48"), 1)));
+ }
+
+ // Extremes of `Instant` range fall outside `LocalDate` range and cannot be
+ // written using Date/Time fields: must retain default handling, not fail
+ @Test
+ public void testInstantExtremes() throws Exception
+ {
+ assertEquals(q("-1000000000-01-01T00:00:00Z"),
+ MAPPER.writeValueAsString(Instant.MIN));
+ assertEquals(DEFAULT_MAPPER.writeValueAsString(Instant.MIN),
+ MAPPER.writeValueAsString(Instant.MIN));
+ assertEquals(q("+1000000000-12-31T23:59:59.999999999Z"),
+ MAPPER.writeValueAsString(Instant.MAX));
+ assertEquals(DEFAULT_MAPPER.writeValueAsString(Instant.MAX),
+ MAPPER.writeValueAsString(Instant.MAX));
+
+ // But values just inside `LocalDate` range are still padded as usual
+ Instant maxLocal = LocalDate.MAX.atStartOfDay().toInstant(ZoneOffset.UTC);
+ assertEquals(q("+999999999-12-31T00:00:00.000Z"),
+ MAPPER.writeValueAsString(maxLocal));
+ Instant minLocal = LocalDate.MIN.atStartOfDay().toInstant(ZoneOffset.UTC);
+ assertEquals(q("-999999999-01-01T00:00:00.000Z"),
+ MAPPER.writeValueAsString(minLocal));
+ }
+
+ // Values written with the feature on must still be readable
+ @Test
+ public void testRoundTrip() throws Exception
+ {
+ for (String raw : new String[] {
+ "2017-09-14T04:28:48Z", "2017-09-14T04:28:48.123456789Z",
+ "1970-01-01T00:00:00Z", "+10000-09-14T04:28:48Z", "-0100-09-14T04:28:48Z" }) {
+ Instant value = Instant.parse(raw);
+ String json = MAPPER.writeValueAsString(value);
+ assertEquals(value, MAPPER.readValue(json, Instant.class),
+ "Round-trip failed for " + raw + " (serialized as " + json + ")");
+ }
+ }
+}
diff --git a/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java
index 84472fe299..a5000f5db0 100644
--- a/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java
+++ b/src/test/java/tools/jackson/databind/ext/javatime/TestFeatures.java
@@ -44,4 +44,11 @@ public void testAdjustDatesToContextTimeZoneSettingEnabledByDefault()
assertTrue(DateTimeFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE.enabledByDefault(),
"Adjust dates to context time zone setting should be enabled by default.");
}
+
+ @Test
+ public void testAlwaysWriteSubSecondDigitsSettingDisabledByDefault()
+ {
+ assertFalse(DateTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS.enabledByDefault(),
+ "Always write sub-second digits setting should be disabled by default.");
+ }
}
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
index 06ff113603..1f2747680e 100644
--- a/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java
+++ b/src/test/java/tools/jackson/databind/ext/javatime/ser/LocalDateTimeSerTest.java
@@ -18,6 +18,7 @@
import java.time.LocalDateTime;
import java.time.Month;
+import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import org.junit.jupiter.api.Test;
@@ -28,6 +29,7 @@
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.ext.javatime.DateTimeTestBase;
import tools.jackson.databind.ext.javatime.MockObjectConfiguration;
+import tools.jackson.databind.module.SimpleModule;
import static org.junit.jupiter.api.Assertions.*;
@@ -41,6 +43,24 @@ static class LDTWrapper {
public LDTWrapper(LocalDateTime v) { value = v; }
}
+ /**
+ * Sub-class that changes the default textual format via {@code _defaultFormatter()};
+ * also has to retain itself through contextualization (see {@code withFormat()}).
+ */
+ static class CustomDefaultLocalDateTimeSerializer extends LocalDateTimeSerializer {
+ private final static DateTimeFormatter DF
+ = DateTimeFormatter.ofPattern("yyyy_MM_dd'X'HH:mm:ss");
+
+ @Override
+ protected DateTimeFormatter _defaultFormatter() { return DF; }
+
+ @Override
+ protected JSR310FormattedSerializerBase