jackson-modules-java8 icon indicating copy to clipboard operation
jackson-modules-java8 copied to clipboard

Date and time serialization are very inconsistent

Open fbacchella opened this issue 2 months ago • 3 comments

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.

fbacchella avatar May 01 '24 19:05 fbacchella