Skip to content

Commit e66b5c1

Browse files
committed
Merge branch '2.x' of github.com:FasterXML/jackson-modules-java8 into 2.x
2 parents ad491b8 + ec2fa91 commit e66b5c1

File tree

5 files changed

+229
-11
lines changed

5 files changed

+229
-11
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ protected InstantDeserializer(Class<T> supportedType,
200200
*/
201201
@Deprecated()
202202
protected InstantDeserializer(Class<T> supportedType,
203-
DateTimeFormatter formatter,
204-
Function<TemporalAccessor, T> parsedToValue,
205-
Function<FromIntegerArguments, T> fromMilliseconds,
206-
Function<FromDecimalArguments, T> fromNanoseconds,
207-
BiFunction<T, ZoneId, T> adjust,
208-
boolean replaceZeroOffsetAsZ
203+
DateTimeFormatter formatter,
204+
Function<TemporalAccessor, T> parsedToValue,
205+
Function<FromIntegerArguments, T> fromMilliseconds,
206+
Function<FromDecimalArguments, T> fromNanoseconds,
207+
BiFunction<T, ZoneId, T> adjust,
208+
boolean replaceZeroOffsetAsZ
209209
) {
210210
this(supportedType, formatter, parsedToValue, fromMilliseconds, fromNanoseconds,
211211
adjust, replaceZeroOffsetAsZ,
@@ -299,8 +299,11 @@ protected InstantDeserializer(InstantDeserializer<T> base,
299299
_alwaysAllowStringifiedDateTimestamps = features.isEnabled(JavaTimeFeature.ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS);
300300
}
301301

302+
/**
303+
* NOTE: {@code public} since 2.21
304+
*/
302305
@Override
303-
protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
306+
public InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
304307
if (dtf == _formatter) {
305308
return this;
306309
}

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerializer.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ public OffsetDateTimeSerializer(OffsetDateTimeSerializer base, Boolean useTimest
3535
super(base, useTimestamp, base._useNanoseconds, formatter, shape);
3636
}
3737

38+
/**
39+
* Method for constructing a new {@code OffsetDateTimeSerializer} with settings
40+
* of this serializer but with custom {@link DateTimeFormatter} overrides.
41+
* Commonly used on {@code INSTANCE} like so:
42+
*<pre>
43+
* DateTimeFormatter dtf = new DateTimeFormatterBuilder()
44+
* .append(DateTimeFormatter.ISO_LOCAL_DATE)
45+
* .appendLiteral('T')
46+
* // and so on
47+
* .toFormatter();
48+
* OffsetDateTimeSerializer ser = OffsetDateTimeSerializer.INSTANCE
49+
* .withFormatter(dtf);
50+
* // register via Module
51+
*</pre>
52+
*
53+
* @since 2.21
54+
*/
55+
public OffsetDateTimeSerializer withFormatter(DateTimeFormatter formatter)
56+
{
57+
return new OffsetDateTimeSerializer(this, _useTimestamp, formatter, _shape);
58+
}
59+
3860
@Override
3961
protected JSR310FormattedSerializerBase<?> withFormat(Boolean useTimestamp,
4062
DateTimeFormatter formatter, JsonFormat.Shape shape)

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
import com.fasterxml.jackson.annotation.JsonFormat;
1515
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
16+
1617
import com.fasterxml.jackson.core.type.TypeReference;
17-
import com.fasterxml.jackson.databind.DeserializationFeature;
18-
import com.fasterxml.jackson.databind.ObjectMapper;
19-
import com.fasterxml.jackson.databind.ObjectReader;
20-
import com.fasterxml.jackson.databind.SerializationFeature;
18+
19+
import com.fasterxml.jackson.databind.*;
20+
import com.fasterxml.jackson.databind.module.SimpleModule;
2121
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
2222
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
2323
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
@@ -873,4 +873,93 @@ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone)
873873
private static String offsetWithoutColon(String string){
874874
return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString();
875875
}
876+
877+
/*
878+
/**********************************************************************
879+
/* Tests for custom formatter (modules-java8#376)
880+
/**********************************************************************
881+
*/
882+
883+
@Test
884+
public void testDeserializationWithCustomFormatter() throws Exception
885+
{
886+
// Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0
887+
DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder()
888+
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
889+
.optionalStart()
890+
.parseLenient()
891+
.appendOffsetId()
892+
.parseStrict()
893+
.optionalEnd()
894+
.parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0)
895+
.toFormatter();
896+
897+
// Create custom deserializer with the custom formatter
898+
InstantDeserializer<OffsetDateTime> customDeserializer =
899+
InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter);
900+
901+
// Create a custom module to override the default deserializer
902+
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
903+
.addDeserializer(OffsetDateTime.class, customDeserializer);
904+
905+
// Add both JavaTimeModule (for other types) and our custom module
906+
// The custom module will override OffsetDateTime deserialization
907+
ObjectMapper mapper = mapperBuilder()
908+
.addModule(customModule)
909+
.build();
910+
911+
// Test deserializing date-time without offset (should default to +00:00)
912+
// This is the main use case from issue #376 - parsing ISO_LOCAL_DATE_TIME with default offset
913+
String jsonWithoutOffset = q("2025-01-01T22:01:05");
914+
OffsetDateTime result = mapper.readValue(jsonWithoutOffset, OffsetDateTime.class);
915+
916+
assertNotNull(result);
917+
assertEquals(2025, result.getYear());
918+
assertEquals(1, result.getMonthValue());
919+
assertEquals(1, result.getDayOfMonth());
920+
assertEquals(22, result.getHour());
921+
assertEquals(1, result.getMinute());
922+
assertEquals(5, result.getSecond());
923+
assertEquals(ZoneOffset.UTC, result.getOffset());
924+
925+
// Test that standard ISO format with offset still works
926+
String jsonWithOffset = q("2025-01-01T22:01:05+02:00");
927+
OffsetDateTime resultWithOffset = mapper.readValue(jsonWithOffset, OffsetDateTime.class);
928+
929+
assertNotNull(resultWithOffset);
930+
assertEquals(2025, resultWithOffset.getYear());
931+
// Verify parsing succeeded - the exact time may be adjusted based on offset conversion
932+
assertTrue(resultWithOffset.toInstant().equals(OffsetDateTime.parse("2025-01-01T22:01:05+02:00").toInstant()));
933+
}
934+
935+
@Test
936+
public void testDeserializationWithCustomFormatterRoundTrip() throws Exception
937+
{
938+
// Create a custom formatter that can parse ISO_LOCAL_DATE_TIME and default offset to 0
939+
DateTimeFormatter customFormatter = new java.time.format.DateTimeFormatterBuilder()
940+
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
941+
.optionalStart()
942+
.parseLenient()
943+
.appendOffsetId()
944+
.parseStrict()
945+
.optionalEnd()
946+
.parseDefaulting(java.time.temporal.ChronoField.OFFSET_SECONDS, 0)
947+
.toFormatter();
948+
949+
InstantDeserializer<OffsetDateTime> customDeserializer =
950+
InstantDeserializer.OFFSET_DATE_TIME.withDateFormat(customFormatter);
951+
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
952+
.addDeserializer(OffsetDateTime.class, customDeserializer);
953+
954+
ObjectMapper mapper = mapperBuilder()
955+
.addModule(customModule)
956+
.build();
957+
958+
// Verify standard ISO format still works
959+
OffsetDateTime original = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 0, ZoneOffset.UTC);
960+
String json = mapper.writeValueAsString(original);
961+
OffsetDateTime roundTripped = mapper.readValue(json, OffsetDateTime.class);
962+
963+
assertIsEqual(original, roundTripped);
964+
}
876965
}

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ser/OffsetDateTimeSerTest.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import java.time.Instant;
44
import java.time.OffsetDateTime;
55
import java.time.ZoneId;
6+
import java.time.ZoneOffset;
67
import java.time.ZonedDateTime;
78
import java.time.format.DateTimeFormatter;
9+
import java.time.format.DateTimeFormatterBuilder;
810
import java.time.temporal.Temporal;
911
import java.util.TimeZone;
1012

@@ -13,6 +15,7 @@
1315
import com.fasterxml.jackson.annotation.JsonFormat;
1416
import com.fasterxml.jackson.databind.ObjectMapper;
1517
import com.fasterxml.jackson.databind.SerializationFeature;
18+
import com.fasterxml.jackson.databind.module.SimpleModule;
1619
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
1720
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
1821
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
@@ -284,4 +287,102 @@ public void testShapeInt() throws Exception {
284287
String json1 = newMapper().writeValueAsString(new Pojo1());
285288
assertEquals("{\"t1\":1651053600000,\"t2\":1651053600.000000000}", json1);
286289
}
290+
291+
/*
292+
/**********************************************************************
293+
/* Tests for custom formatter (modules-java8#376)
294+
/**********************************************************************
295+
*/
296+
297+
@Test
298+
public void testSerializationWithCustomFormatter() throws Exception
299+
{
300+
// Create a custom formatter that displays only 3 digits of nano-seconds instead of 9
301+
// Use ISO_LOCAL_DATE and ISO_LOCAL_TIME separately to control nanosecond precision
302+
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
303+
.append(DateTimeFormatter.ISO_LOCAL_DATE)
304+
.appendLiteral('T')
305+
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
306+
.appendLiteral(':')
307+
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
308+
.optionalStart()
309+
.appendLiteral(':')
310+
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
311+
.optionalStart()
312+
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true)
313+
.optionalEnd()
314+
.optionalEnd()
315+
.appendOffsetId()
316+
.toFormatter();
317+
318+
// Create a date with nanoseconds (123456789 nanos = .123456789 seconds)
319+
OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC);
320+
String json = _mapper(customFormatter).writeValueAsString(date);
321+
322+
// Should output with only 3 digits of nano precision (.123 instead of .123456789)
323+
assertEquals(q("2025-01-01T22:01:05.123Z"), json);
324+
}
325+
326+
@Test
327+
public void testSerializationWithCustomFormatterNoNanos() throws Exception
328+
{
329+
// Create a formatter without nanoseconds
330+
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
331+
.append(DateTimeFormatter.ISO_LOCAL_DATE)
332+
.appendLiteral('T')
333+
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
334+
.appendLiteral(':')
335+
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
336+
.optionalStart()
337+
.appendLiteral(':')
338+
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
339+
.optionalEnd()
340+
.appendOffsetId()
341+
.toFormatter();
342+
343+
OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.UTC);
344+
String json = _mapper(customFormatter).writeValueAsString(date);
345+
346+
// Should output without nanoseconds
347+
assertEquals(q("2025-01-01T22:01:05Z"), json);
348+
}
349+
350+
@Test
351+
public void testSerializationWithCustomFormatterAndOffset() throws Exception
352+
{
353+
// Create a custom formatter that displays only 3 digits of nano-seconds
354+
DateTimeFormatter customFormatter = new DateTimeFormatterBuilder()
355+
.append(DateTimeFormatter.ISO_LOCAL_DATE)
356+
.appendLiteral('T')
357+
.appendValue(java.time.temporal.ChronoField.HOUR_OF_DAY, 2)
358+
.appendLiteral(':')
359+
.appendValue(java.time.temporal.ChronoField.MINUTE_OF_HOUR, 2)
360+
.optionalStart()
361+
.appendLiteral(':')
362+
.appendValue(java.time.temporal.ChronoField.SECOND_OF_MINUTE, 2)
363+
.optionalStart()
364+
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 3, 3, true)
365+
.optionalEnd()
366+
.optionalEnd()
367+
.appendOffsetId()
368+
.toFormatter();
369+
370+
371+
// Create a date with a non-UTC offset
372+
OffsetDateTime date = OffsetDateTime.of(2025, 1, 1, 22, 1, 5, 123456789, ZoneOffset.ofHours(5));
373+
String json = _mapper(customFormatter).writeValueAsString(date);
374+
375+
// Should output with offset +05:00 and 3 digits of nano precision
376+
assertEquals(q("2025-01-01T22:01:05.123+05:00"), json);
377+
}
378+
379+
private ObjectMapper _mapper(DateTimeFormatter dtf) {
380+
OffsetDateTimeSerializer customSerializer = OffsetDateTimeSerializer.INSTANCE
381+
.withFormatter(dtf);
382+
SimpleModule customModule = new SimpleModule("CustomOffsetDateTimeModule")
383+
.addSerializer(OffsetDateTime.class, customSerializer);
384+
return mapperBuilder()
385+
.addModule(customModule)
386+
.build();
387+
}
287388
}

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Modules:
1414
negative timestamps incorrectly
1515
(reported by Kevin M)
1616
(fix by @cowtowncoder, w/ Claude code)
17+
#376: Allow specifying custom `DateTimeFormatter` for `OffsetDateTime` ser/deser
18+
(new constructors?)
19+
(requested by @ZIRAKrezovic)
1720

1821
No changes since 2.20
1922

0 commit comments

Comments
 (0)