jackson-modules-java8
jackson-modules-java8 copied to clipboard
Date and time serialization are very inconsistent
I’m comparing results of different time object serialization with different settings and the result are very different.
I wrote the following code
package com.axibase.date;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.function.Consumer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
public class JacksonConcistency {
private static TimeZone defaultTz;
@BeforeClass
public static void saveTimeZone() {
defaultTz = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("Japan/Tokyo"));
}
@AfterClass
public static void restoreTimeZone() {
TimeZone.setDefault(defaultTz);
}
public JsonMapper getMapper(Consumer<JsonMapper.Builder> configurator) {
JsonMapper.Builder builder = JsonMapper.builder();
builder.setDefaultTyping(StdTypeResolverBuilder.noTypeInfoBuilder());
builder.addModule(new JavaTimeModule());
builder.addModule(new Jdk8Module());
builder.addModule(new AfterburnerModule());
configurator.accept(builder);
return builder.build();
}
public void runTest(Object value, Map.Entry<String, Consumer<JsonMapper>> mapperConfigurator) {
try {
JsonMapper simpleMapper = getMapper(m -> {});
JsonMapper axibaseMapper = getMapper(m -> m.addModule(new JacksonModule()));
mapperConfigurator.getValue().accept(simpleMapper);
mapperConfigurator.getValue().accept(axibaseMapper);
ObjectWriter simpleWritter = simpleMapper.writerFor(Object.class);
ObjectWriter simpleWritter = simpleMapper.writerFor(Object.class);
System.err.format(" %s %s%n", value.getClass().getName(), simpleWritter.writeValueAsString(value));
axibaseWritter.writeValueAsString(value));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
private Calendar fromInstant(Instant i, ZoneId tz) {
return new Calendar.Builder().setCalendarType("iso8601").setInstant(i.toEpochMilli()).setTimeZone(TimeZone.getTimeZone(tz)).build();
}
@Test
public void testDefaultSettigs() {
List<Map.Entry<String, Consumer<JsonMapper>>> configurators = List.of(
Map.entry("TIMESTAMPS_AS_MILLISECONDS",
m -> m.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)),
Map.entry("TIMESTAMPS_AS_NANOSECONDS",
m -> m.enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)),
Map.entry("AS_STRING",
m -> {
m.setTimeZone(TimeZone.getTimeZone("America/New_York"));
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
m.disable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
m.disable(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE);
}
),
Map.entry("WITH_ZONE_ID",
m -> {
m.setTimeZone(TimeZone.getTimeZone("America/New_York"));
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
m.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
m.disable(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE);
}
),
Map.entry("WITH_CONTEXT_TIME_ZONE",
m -> {
m.setTimeZone(TimeZone.getTimeZone("America/New_York"));
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
m.disable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
m.enable(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE);
}
),
Map.entry("WITH_CONTEXT_TIME_ZONE_AND_ID",
m -> {
m.setTimeZone(TimeZone.getTimeZone("America/New_York"));
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
m.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
m.enable(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE);
}
)
);
ZoneId moscow = ZoneId.of("Europe/Moscow");
long seconds = 1714421498L;
long withNanos = 936_155_001L;
long withMilli = 936_000_000L;
long withoutSubSecond = 0L;
List<Map.Entry<String, List<Object>>> values = List.of(
Map.entry("Instant with nano",
List.of(Instant.ofEpochSecond(seconds, withNanos))
),
Map.entry("Instant with milli",
List.of(Instant.ofEpochSecond(seconds, withMilli),
Date.from(Instant.ofEpochSecond(seconds, withMilli))
)
),
Map.entry("Instant without subseconds",
List.of(Instant.ofEpochSecond(seconds, withoutSubSecond),
Date.from(Instant.ofEpochSecond(seconds, withoutSubSecond))
)
),
Map.entry("Date with nano",
List.of(Instant.ofEpochSecond(seconds, withNanos).atZone(moscow))
),
Map.entry("Date with milli",
List.of(Instant.ofEpochSecond(seconds, withMilli).atZone(moscow),
fromInstant(Instant.ofEpochSecond(seconds, withMilli), moscow)
)
),
Map.entry("Date without subseconds",
List.of(Instant.ofEpochSecond(seconds, withoutSubSecond).atZone(moscow),
fromInstant(Instant.ofEpochSecond(seconds, withoutSubSecond), moscow)
)
)
);
for (Map.Entry<String, Consumer<JsonMapper>> c: configurators) {
System.err.format("**** %s ****%n", c.getKey());
for (Map.Entry<String, List<Object>> v: values) {
System.err.format("%s%n", v.getKey());
for (Object o: v.getValue()) {
runTest(o, c);
}
}
System.err.println();
}
}
}
The output is
**** TIMESTAMPS_AS_MILLISECONDS ****
Instant with nano
java.time.Instant 1714421498936
Instant with milli
java.time.Instant 1714421498936
java.util.Date 1714421498936
Instant without subseconds
java.time.Instant 1714421498000
java.util.Date 1714421498000
Date with nano
java.time.ZonedDateTime 1714421498936
Date with milli
java.time.ZonedDateTime 1714421498936
java.util.GregorianCalendar 1714421498936
Date without subseconds
java.time.ZonedDateTime 1714421498000
java.util.GregorianCalendar 1714421498000
**** TIMESTAMPS_AS_NANOSECONDS ****
Instant with nano
java.time.Instant 1714421498.936155001
Instant with milli
java.time.Instant 1714421498.936000000
java.util.Date 1714421498936
Instant without subseconds
java.time.Instant 1714421498.000000000
java.util.Date 1714421498000
Date with nano
java.time.ZonedDateTime 1714421498.936155001
Date with milli
java.time.ZonedDateTime 1714421498.936000000
java.util.GregorianCalendar 1714421498936
Date without subseconds
java.time.ZonedDateTime 1714421498.000000000
java.util.GregorianCalendar 1714421498000
**** AS_STRING ****
Instant with nano
java.time.Instant "2024-04-29T20:11:38.936155001Z"
Instant with milli
java.time.Instant "2024-04-29T20:11:38.936Z"
java.util.Date "2024-04-29T16:11:38.936-04:00"
Instant without subseconds
java.time.Instant "2024-04-29T20:11:38Z"
java.util.Date "2024-04-29T16:11:38.000-04:00"
Date with nano
java.time.ZonedDateTime "2024-04-29T23:11:38.936155001+03:00"
Date with milli
java.time.ZonedDateTime "2024-04-29T23:11:38.936+03:00"
java.util.GregorianCalendar "2024-04-29T16:11:38.936-04:00"
Date without subseconds
java.time.ZonedDateTime "2024-04-29T23:11:38+03:00"
java.util.GregorianCalendar "2024-04-29T16:11:38.000-04:00"
**** WITH_ZONE_ID ****
Instant with nano
java.time.Instant "2024-04-29T20:11:38.936155001Z"
Instant with milli
java.time.Instant "2024-04-29T20:11:38.936Z"
java.util.Date "2024-04-29T16:11:38.936-04:00"
Instant without subseconds
java.time.Instant "2024-04-29T20:11:38Z"
java.util.Date "2024-04-29T16:11:38.000-04:00"
Date with nano
java.time.ZonedDateTime "2024-04-29T23:11:38.936155001+03:00[Europe/Moscow]"
Date with milli
java.time.ZonedDateTime "2024-04-29T23:11:38.936+03:00[Europe/Moscow]"
java.util.GregorianCalendar "2024-04-29T16:11:38.936-04:00"
Date without subseconds
java.time.ZonedDateTime "2024-04-29T23:11:38+03:00[Europe/Moscow]"
java.util.GregorianCalendar "2024-04-29T16:11:38.000-04:00"
**** WITH_CONTEXT_TIME_ZONE ****
Instant with nano
java.time.Instant "2024-04-29T20:11:38.936155001Z"
Instant with milli
java.time.Instant "2024-04-29T20:11:38.936Z"
java.util.Date "2024-04-29T16:11:38.936-04:00"
Instant without subseconds
java.time.Instant "2024-04-29T20:11:38Z"
java.util.Date "2024-04-29T16:11:38.000-04:00"
Date with nano
java.time.ZonedDateTime "2024-04-29T16:11:38.936155001-04:00"
Date with milli
java.time.ZonedDateTime "2024-04-29T16:11:38.936-04:00"
java.util.GregorianCalendar "2024-04-29T16:11:38.936-04:00"
Date without subseconds
java.time.ZonedDateTime "2024-04-29T16:11:38-04:00"
java.util.GregorianCalendar "2024-04-29T16:11:38.000-04:00"
**** WITH_CONTEXT_TIME_ZONE_AND_ID ****
Instant with nano
java.time.Instant "2024-04-29T20:11:38.936155001Z"
Instant with milli
java.time.Instant "2024-04-29T20:11:38.936Z"
java.util.Date "2024-04-29T16:11:38.936-04:00"
Instant without subseconds
java.time.Instant "2024-04-29T20:11:38Z"
java.util.Date "2024-04-29T16:11:38.000-04:00"
Date with nano
java.time.ZonedDateTime "2024-04-29T23:11:38.936155001+03:00[Europe/Moscow]"
Date with milli
java.time.ZonedDateTime "2024-04-29T23:11:38.936+03:00[Europe/Moscow]"
java.util.GregorianCalendar "2024-04-29T16:11:38.936-04:00"
Date without subseconds
java.time.ZonedDateTime "2024-04-29T23:11:38+03:00[Europe/Moscow]"
java.util.GregorianCalendar "2024-04-29T16:11:38.000-04:00"
I don’t think the output should depend of the class used, except for the precision. And many are plain wrong, where many features are not really applied. For example with WRITE_DATES_WITH_CONTEXT_TIME_ZONE
, even Instant should not be serialized az UTC, but indeed with the context time zone.