Skip to content
Merged
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,40 @@ 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} <b>values</b>
* 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>
* Also note that this only applies to values, and NOT to {@link java.util.Map}
* keys: date/time keys keep being written using the JDK-provided ISO formatters,
* so a zero sub-second value is still omitted there. Types other than the four
* listed above -- notably {@link java.time.LocalTime} and
* {@link java.time.OffsetTime}, whose ISO formats also omit the seconds field --
* are likewise unaffected.
*<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 @@ -27,7 +27,7 @@ public abstract class JSR310DateTimeDeserializerBase<T>
protected final DateTimeFormatter _formatter;

/**
* Setting that indicates the {@Link JsonFormat.Shape} specified for this deserializer
* 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
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 @@ -47,8 +47,6 @@
public abstract class InstantSerializerBase<T extends Temporal>
extends JSR310FormattedSerializerBase<T>
{
private final DateTimeFormatter defaultFormat;

private final ToLongFunction<T> getEpochMillis;

private final ToLongFunction<T> getEpochSeconds;
Expand All @@ -61,8 +59,7 @@ protected InstantSerializerBase(Class<T> supportedType, ToLongFunction<T> getEpo
{
// 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;
super(supportedType, null, defaultFormat);
this.getEpochMillis = getEpochMillis;
this.getEpochSeconds = getEpochSeconds;
this.getNanoseconds = getNanoseconds;
Expand All @@ -86,7 +83,18 @@ protected InstantSerializerBase(InstantSerializerBase<T> base,
protected InstantSerializerBase(InstantSerializerBase<T> base, Boolean useTimestamp,
Boolean useNanoseconds, DateTimeFormatter dtf, JsonFormat.Shape shape) {
super(base, useTimestamp, useNanoseconds, dtf, shape);
defaultFormat = base.defaultFormat;
getEpochMillis = base.getEpochMillis;
getEpochSeconds = base.getEpochSeconds;
getNanoseconds = base.getNanoseconds;
}

/**
* @since 2.23
*/
protected InstantSerializerBase(InstantSerializerBase<T> base,
DateTimeFormatter defaultFormat)
{
super(base, defaultFormat);
getEpochMillis = base.getEpochMillis;
getEpochSeconds = base.getEpochSeconds;
getNanoseconds = base.getNanoseconds;
Expand Down Expand Up @@ -147,7 +155,7 @@ protected JsonToken serializationShape(SerializerProvider provider) {
// @since 2.12
protected String formatValue(T value, SerializerProvider provider)
{
DateTimeFormatter formatter = (_formatter == null) ? defaultFormat :_formatter;
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ abstract class JSR310FormattedSerializerBase<T>
*/
protected final DateTimeFormatter _formatter;

/**
* Format to use when no explicit {@link #_formatter} is configured. Unlike
* {@code _formatter}, a non-null value here does NOT force serialization as a
* JSON String -- which is exactly why the two cannot be collapsed into one.
*<p>
* May be {@code null}, in which case the sub-class decides the fallback
* (typically either a JDK {@code ISO_*} constant or {@code value.toString()}).
*
* @since 2.23
*/
protected final DateTimeFormatter _defaultFormat;

protected final JsonFormat.Shape _shape;

/**
Expand All @@ -81,13 +93,22 @@ protected JSR310FormattedSerializerBase(Class<T> supportedType) {

protected JSR310FormattedSerializerBase(Class<T> supportedType,
DateTimeFormatter formatter) {
this(supportedType, formatter, null);
}

/**
* @since 2.23
*/
protected JSR310FormattedSerializerBase(Class<T> supportedType,
DateTimeFormatter formatter, DateTimeFormatter defaultFormat) {
super(supportedType);
_useTimestamp = null;
_useNanoseconds = null;
_shape = null;
_formatter = formatter;
_defaultFormat = defaultFormat;
}

protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase<?> base,
Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape)
{
Expand All @@ -103,6 +124,27 @@ protected JSR310FormattedSerializerBase(JSR310FormattedSerializerBase<?> base,
_useNanoseconds = useNanoseconds;
_formatter = dtf;
_shape = shape;
_defaultFormat = base._defaultFormat;
}

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

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,27 +41,47 @@ public class LocalDateTimeSerializer extends JSR310FormattedSerializerBase<Local
private static final long serialVersionUID = 1L;

public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();

protected LocalDateTimeSerializer() {
this(null);
}

public LocalDateTimeSerializer(DateTimeFormatter f) {
super(LocalDateTime.class, f);
super(LocalDateTime.class, f, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}

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

/**
* @since 2.23
*/
protected LocalDateTimeSerializer(LocalDateTimeSerializer 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 LocalDateTimeSerializer withFeatures(JacksonFeatureSet<JavaTimeFeature> features) {
if (features.isEnabled(JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS)) {
return new LocalDateTimeSerializer(this, SubSecondFormatters.LOCAL_DATE_TIME);
}
return this;
}

@Override
protected JSR310FormattedSerializerBase<LocalDateTime> withFormat(Boolean useTimestamp, DateTimeFormatter f, JsonFormat.Shape shape) {
return new LocalDateTimeSerializer(this, useTimestamp, _useNanoseconds, f);
}

protected DateTimeFormatter _defaultFormatter() {
return DateTimeFormatter.ISO_LOCAL_DATE_TIME;
return _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
Loading
Loading