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 @@ -200,12 +200,12 @@ protected InstantDeserializer(Class<T> supportedType,
*/
@Deprecated()
protected InstantDeserializer(Class<T> supportedType,
DateTimeFormatter formatter,
Function<TemporalAccessor, T> parsedToValue,
Function<FromIntegerArguments, T> fromMilliseconds,
Function<FromDecimalArguments, T> fromNanoseconds,
BiFunction<T, ZoneId, T> adjust,
boolean replaceZeroOffsetAsZ
DateTimeFormatter formatter,
Function<TemporalAccessor, T> parsedToValue,
Function<FromIntegerArguments, T> fromMilliseconds,
Function<FromDecimalArguments, T> fromNanoseconds,
BiFunction<T, ZoneId, T> adjust,
boolean replaceZeroOffsetAsZ
) {
this(supportedType, formatter, parsedToValue, fromMilliseconds, fromNanoseconds,
adjust, replaceZeroOffsetAsZ,
Expand Down Expand Up @@ -299,8 +299,11 @@ protected InstantDeserializer(InstantDeserializer<T> base,
_alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS);
}

/**
* NOTE: {@code public} since 2.21
*/
@Override
protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
public InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix for deserialization.

if (dtf == _formatter) {
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest
super(base, useTimestamp, base._useNanoseconds, formatter, shape);
}

/**
* Method for constructing a new {@code OffsetDateTimeSerializer} with settings
* of this serializer but with custom {@link DateTimeFormatter} overrides.
* Commonly used on {@code INSTANCE} like so:
*<pre>
* DateTimeFormatter dtf = new DateTimeFormatterBuilder()
* .append(DateTimeFormatter.ISO_LOCAL_DATE)
* .appendLiteral('T')
* // and so on
* .toFormatter();
* OffsetDateTimeSerializer ser = OffsetDateTimeSerializer.INSTANCE
* .withFormatter(dtf);
* // register via Module
*</pre>
*
* @since 2.21
*/
public OffsetDateTimeSerializer withFormatter(DateTimeFormatter formatter)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix for serialization.

{
return new OffsetDateTimeSerializer(this, _useTimestamp, formatter, _shape);
}

@Override
protected JSR310FormattedSerializerBase<?> withFormat(Boolean useTimestamp,
DateTimeFormatter formatter, JsonFormat.Shape shape)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.SerializationFeature;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
Expand Down Expand Up @@ -873,4 +873,93 @@ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone)
private static String offsetWithoutColon(String string){
return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString();
}

/*
/**********************************************************************
/* Tests for custom formatter (modules-java8#376)
/**********************************************************************
*/

@Test
public void testDeserializationWithCustomFormatter() throws Exception
{
// Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0
DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.optionalStart()
.parseLenient()
.appendOffsetId()
.parseStrict()
.optionalEnd()
.parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0)
.toFormatter();

// Create custom deserializer with the custom formatter
InstantDeserializer<OffsetDateTime> customDeserializer =
InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter);

// Create a custom module to override the default deserializer
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
.addDeserializer(OffsetDateTime.class, customDeserializer);

// Add both JavaTimeModule (for other types) and our custom module
// The custom module will override OffsetDateTime deserialization
ObjectMapper mapper = mapperBuilder()
.addModule(customModule)
.build();

// Test deserializing date-time without offset (should default to +00:00)
// This is the main use case from issue #376 - parsing ISO_LOCAL_DATE_TIME with default offset
String jsonWithoutOffset = q("2025-01-01T22:01:05");
OffsetDateTime result = mapper.readValue(jsonWithoutOffset, OffsetDateTime.class);

assertNotNull(result);
assertEquals(2025, result.getYear());
assertEquals(1, result.getMonthValue());
assertEquals(1, result.getDayOfMonth());
assertEquals(22, result.getHour());
assertEquals(1, result.getMinute());
assertEquals(5, result.getSecond());
assertEquals(ZoneOffset.UTC, result.getOffset());

// Test that standard ISO format with offset still works
String jsonWithOffset = q("2025-01-01T22:01:05+02:00");
OffsetDateTime resultWithOffset = mapper.readValue(jsonWithOffset, OffsetDateTime.class);

assertNotNull(resultWithOffset);
assertEquals(2025, resultWithOffset.getYear());
// Verify parsing succeeded - the exact time may be adjusted based on offset conversion
assertTrue(resultWithOffset.toInstant().equals(OffsetDateTime.parse("2025-01-01T22:01:05+02:00").toInstant()));
}

@Test
public void testDeserializationWithCustomFormatterRoundTrip() throws Exception
{
// Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0
DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.optionalStart()
.parseLenient()
.appendOffsetId()
.parseStrict()
.optionalEnd()
.parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0)
.toFormatter();

InstantDeserializer<OffsetDateTime> customDeserializer =
InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter);
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
.addDeserializer(OffsetDateTime.class, customDeserializer);

ObjectMapper mapper = mapperBuilder()
.addModule(customModule)
.build();

// Verify standard ISO format still works
OffsetDateTime original = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 0, ZoneOffset.UTC);
String json = mapper.writeValueAsString(original);
OffsetDateTime roundTripped = mapper.readValue(json, OffsetDateTime.class);

assertIsEqual(original, roundTripped);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.Temporal;
import java.util.TimeZone;

Expand All @@ -13,6 +15,7 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
Expand Down Expand Up @@ -284,4 +287,102 @@ public void testShapeInt() throws Exception {
String json1 = newMapper().writeValueAsString(new Pojo1());
assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1);
}

/*
/**********************************************************************
/* Tests for custom formatter (modules-java8#376)
/**********************************************************************
*/

@Test
public void testSerializationWithCustomFormatter() throws Exception
{
// Create a custom formatter that displays only 3 digits of nano-seconds instead of 9
// Use ISO_LOCAL_DATE and ISO_LOCAL_TIME separately to control nanosecond precision
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
.optionalStart()
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true)
.optionalEnd()
.optionalEnd()
.appendOffsetId()
.toFormatter();

// Create a date with nanoseconds (123456789 nanos = .123456789 seconds)
OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC);
String json = _mapper(customFormatter).writeValueAsString(date);

// Should output with only 3 digits of nano precision (.123 instead of .123456789)
assertEquals(q("2025-01-01T22:01:05.123Z"), json);
}

@Test
public void testSerializationWithCustomFormatterNoNanos() throws Exception
{
// Create a formatter without nanoseconds
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
.optionalEnd()
.appendOffsetId()
.toFormatter();

OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC);
String json = _mapper(customFormatter).writeValueAsString(date);

// Should output without nanoseconds
assertEquals(q("2025-01-01T22:01:05Z"), json);
}

@Test
public void testSerializationWithCustomFormatterAndOffset() throws Exception
{
// Create a custom formatter that displays only 3 digits of nano-seconds
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
.optionalStart()
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true)
.optionalEnd()
.optionalEnd()
.appendOffsetId()
.toFormatter();


// Create a date with a non-UTC offset
OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.ofHours(5));
String json = _mapper(customFormatter).writeValueAsString(date);

// Should output with offset +05:00 and 3 digits of nano precision
assertEquals(q("2025-01-01T22:01:05.123+05:00"), json);
}

private ObjectMapper _mapper(DateTimeFormatter dtf) {
OffsetDateTimeSerializer customSerializer = OffsetDateTimeSerializer.INSTANCE
.withFormatter(dtf);
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
.addSerializer(OffsetDateTime.class, customSerializer);
return mapperBuilder()
.addModule(customModule)
.build();
}
}
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Modules:
negative timestamps incorrectly
(reported by Kevin M)
(fix by @cowtowncoder, w/ Claude code)
#376: Allow specifying custom `DateTimeFormatter` for `OffsetDateTime` ser/deser
(new constructors?)
(requested by @ZIRAKrezovic)

No changes since 2.20

Expand Down
Loading