Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,33 @@ public enum JavaTimeFeature implements JacksonFeature
*<p>
* Default setting is false, meaning that Month is serialized/deserialized as a zero-based index.
*/
ONE_BASED_MONTHS(false)
ONE_BASED_MONTHS(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.
*<p>
* 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).
*<p>
* 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: a value with microsecond or nanosecond precision is written with
* 6 or 9 digits respectively, so no information is lost.
*<p>
* 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.
*<p>
* Default setting is disabled, for backwards compatibility.
*
* @since 2.23
*/
ALWAYS_WRITE_SUBSECOND_DIGITS(false)
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ public void setupModule(SetupContext context) {
JavaTimeSerializers sers = new JavaTimeSerializers();

sers.addSerializer(Duration.class, DurationSerializer.INSTANCE);
sers.addSerializer(Instant.class, InstantSerializer.INSTANCE);
sers.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE);
sers.addSerializer(Instant.class, InstantSerializer.INSTANCE.withFeatures(_features));
sers.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE.withFeatures(_features));
sers.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
sers.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
sers.addSerializer(MonthDay.class, MonthDaySerializer.INSTANCE);
sers.addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE);
sers.addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE.withFeatures(_features));
sers.addSerializer(OffsetTime.class, OffsetTimeSerializer.INSTANCE);
sers.addSerializer(Period.class, new ToStringSerializer(Period.class));
sers.addSerializer(Year.class, YearSerializer.INSTANCE);
Expand All @@ -173,7 +173,7 @@ public void setupModule(SetupContext context) {
* serialization with timezone offset only, not timezone id.
* But this is configurable.
*/
sers.addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE);
sers.addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE.withFeatures(_features));

// since 2.11: need to override Type Id handling
// (actual concrete type is `ZoneRegion`, but that's not visible)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;

/**
* Serializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s.
*
Expand All @@ -34,10 +38,20 @@ public class InstantSerializer extends InstantSerializerBase<Instant>

public static final InstantSerializer INSTANCE = new InstantSerializer();

/**
* Whether {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}
* is enabled: if so, the default representation is padded to at least 3 sub-second
* digits.
*
* @since 2.23
*/
private final boolean _alwaysWriteSubsecondDigits;

protected InstantSerializer() {
super(Instant.class, Instant::toEpochMilli, Instant::getEpochSecond, Instant::getNano,
// null -> use 'value.toString()', default format
null);
_alwaysWriteSubsecondDigits = false;
}

@Deprecated // since 2.14
Expand All @@ -52,11 +66,63 @@ protected InstantSerializer(InstantSerializer base,
protected InstantSerializer(InstantSerializer base, Boolean useTimestamp,
DateTimeFormatter formatter, JsonFormat.Shape shape) {
super(base, useTimestamp, base._useNanoseconds, formatter, shape);
_alwaysWriteSubsecondDigits = base._alwaysWriteSubsecondDigits;
}

protected InstantSerializer(InstantSerializer base,
Boolean useTimestamp, Boolean useNanoseconds, DateTimeFormatter formatter) {
super(base, useTimestamp, useNanoseconds, formatter);
_alwaysWriteSubsecondDigits = base._alwaysWriteSubsecondDigits;
}

/**
* @since 2.23
*/
protected InstantSerializer(InstantSerializer base, boolean alwaysWriteSubsecondDigits) {
super(base, base._useTimestamp, base._useNanoseconds, base._formatter, base._shape);
_alwaysWriteSubsecondDigits = alwaysWriteSubsecondDigits;
}

/**
* Method called by {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeModule}
* to apply module-level {@link JavaTimeFeature} settings.
*
* @since 2.23
*/
public InstantSerializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (features.isEnabled(JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
return new InstantSerializer(this, true);
}
return this;
}

/**
* Overridden to implement
* {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}
* by padding the default representation, instead of swapping in a different formatter.
*<p>
* Rationale: the default representation is {@link Instant#toString()}, that is,
* {@link DateTimeFormatter#ISO_INSTANT}, which writes exactly 0, 3, 6 or 9 sub-second
* digits -- so the only case needing a fix is the zero one. Formatting through a
* zone-bound {@code DateTimeFormatter} instead would resolve the value via
* {@link java.time.LocalDateTime}, whose year range is narrower than that of
* {@code Instant}, and would thereby fail for {@link Instant#MIN} / {@link Instant#MAX}.
*
* @since 2.23
*/
@Override
protected String formatValue(Instant value, SerializerProvider provider)
{
String formatted = super.formatValue(value, provider);
// Only applies to the default representation: an explicit formatter wins
if (_alwaysWriteSubsecondDigits && (_formatter == null) && (value.getNano() == 0)) {
final int last = formatted.length() - 1;
// Defensive: `ISO_INSTANT` always ends in 'Z', but do not corrupt output if not
if ((last >= 0) && (formatted.charAt(last) == 'Z')) {
formatted = formatted.substring(0, last) + ".000Z";
}
}
return formatted;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ protected InstantSerializerBase(InstantSerializerBase<T> base, Boolean useTimest
getNanoseconds = base.getNanoseconds;
}

/**
* Copy-constructor used for replacing the "hidden" default formatter -- and only
* that -- of an existing serializer; needed for
* {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}.
* Note that the default formatter must NOT be passed as {@code _formatter}, since
* a non-null {@code _formatter} also forces serialization as a JSON String.
*
* @since 2.23
*/
protected InstantSerializerBase(InstantSerializerBase<T> base,
DateTimeFormatter defaultFormat)
{
super(base, base._useTimestamp, base._useNanoseconds, base._formatter, base._shape);
this.defaultFormat = defaultFormat;
getEpochMillis = base.getEpochMillis;
getEpochSeconds = base.getEpochSeconds;
getNanoseconds = base.getNanoseconds;
}

@Override
protected abstract JSR310FormattedSerializerBase<?> withFormat(
Boolean useTimestamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;

/**
* Serializer for Java 8 temporal {@link LocalDateTime}s.
Expand All @@ -39,18 +41,49 @@ public class LocalDateTimeSerializer extends JSR310FormattedSerializerBase<Local
private static final long serialVersionUID = 1L;

public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();


/**
* Formatter to use instead of {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} when no
* explicit format is configured; {@code null} for the default.
*
* @since 2.23
*/
protected final DateTimeFormatter _defaultFormat;

protected LocalDateTimeSerializer() {
this(null);
}

public LocalDateTimeSerializer(DateTimeFormatter f) {
super(LocalDateTime.class, f);
_defaultFormat = null;
}

// protected in 2.14 (from private)
protected LocalDateTimeSerializer(LocalDateTimeSerializer base, Boolean useTimestamp, Boolean useNanoseconds, DateTimeFormatter f) {
super(base, useTimestamp, useNanoseconds, f, null);
_defaultFormat = base._defaultFormat;
}

/**
* @since 2.23
*/
protected LocalDateTimeSerializer(LocalDateTimeSerializer base, DateTimeFormatter defaultFormat) {
super(base, base._useTimestamp, base._useNanoseconds, base._formatter, base._shape);
_defaultFormat = defaultFormat;
}

/**
* Method called by {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeModule}
* to apply module-level {@link JavaTimeFeature} settings.
*
* @since 2.23
*/
public LocalDateTimeSerializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (features.isEnabled(JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
return new LocalDateTimeSerializer(this, SubSecondFormatters.LOCAL_DATE_TIME);
}
return this;
}

@Override
Expand All @@ -59,7 +92,7 @@ protected JSR310FormattedSerializerBase<LocalDateTime> withFormat(Boolean useTim
}

protected DateTimeFormatter _defaultFormatter() {
return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
return (_defaultFormat == null) ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : _defaultFormat;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature;

public class OffsetDateTimeSerializer extends InstantSerializerBase<OffsetDateTime>
{
private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -35,6 +38,27 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest
super(base, useTimestamp, base._useNanoseconds, formatter, shape);
}

/**
* @since 2.23
*/
protected OffsetDateTimeSerializer(OffsetDateTimeSerializer base,
DateTimeFormatter defaultFormat) {
super(base, defaultFormat);
}

/**
* Method called by {@link com.fasterxml.jackson.datatype.jsr310.JavaTimeModule}
* to apply module-level {@link JavaTimeFeature} settings.
*
* @since 2.23
*/
public OffsetDateTimeSerializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (features.isEnabled(JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
return new OffsetDateTimeSerializer(this, SubSecondFormatters.OFFSET_DATE_TIME);
}
return this;
}

/**
* Method for constructing a new {@code OffsetDateTimeSerializer} with settings
* of this serializer but with custom {@link DateTimeFormatter} overrides.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.fasterxml.jackson.datatype.jsr310.ser;

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 com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature#ALWAYS_WRITE_SUBSECOND_DIGITS}
* is enabled.
*<p>
* 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).
*<p>
* Note that there is deliberately no counterpart of {@link DateTimeFormatter#ISO_INSTANT}
* here: formatting an {@link java.time.Instant} through a zone-bound formatter goes via
* {@link java.time.LocalDateTime}, whose year range is narrower than {@code Instant}'s,
* so {@link java.time.Instant#MIN} / {@link java.time.Instant#MAX} would fail. See
* {@link InstantSerializer#formatValue} for the handling used instead.
*
* @since 2.23
*/
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}.
*/
public final static DateTimeFormatter LOCAL_DATE_TIME = _localDateTimeBuilder()
.toFormatter();

/**
* Counterpart of {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}.
*/
public final static 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.
*/
public final static DateTimeFormatter ZONED_DATE_TIME = new DateTimeFormatterBuilder()
.append(OFFSET_DATE_TIME)
.optionalStart()
.appendLiteral('[')
.parseCaseSensitive()
.appendZoneRegionId()
.appendLiteral(']')
.toFormatter();
}
Loading