15

I've just converted over many of my Dates to LocalDateTime per the new(ish) java 8 time package. I've enjoyed the switch so far until I started trying to serialize and deserialize.

How can I configure Jackson to support them?:

LocalDateTime --serialize--> UTC Timestamp --deserialize--> LocalDateTime?

There's plenty of material on here about converting to formatted strings, but I can't seem to find an out-of-the-box solution to utc timestamps.

xingbin
  • 27,410
  • 9
  • 53
  • 103
Daniel Patrick
  • 3,980
  • 6
  • 29
  • 49
  • This should help: https://fasterxml.github.io/jackson-databind/javadoc/2.8/com/fasterxml/jackson/databind/SerializationFeature.html#WRITE_DATES_AS_TIMESTAMPS – assylias Feb 01 '19 at 12:43
  • 3
    If it's UTC timestamps, why not use `Instant` instead of `LocalDateTime`? – ernest_k Feb 01 '19 at 12:44
  • @assylias, believe it or not that doesn't work: WRITE_DATES_AS_TIMESTAMPS applies to Dates. When turned on for LocalDateTimes Jackson serializes it to an array of values. – Daniel Patrick Feb 01 '19 at 12:54
  • @ernest_k, I have other reasons for wanting to use LocalDateTime. A third party API wants it formatted as a timestamp integer though, so I need to convert it to interact with their system. – Daniel Patrick Feb 01 '19 at 12:55
  • Have you tried [FasterXML/jackson-modules-java8](https://github.com/FasterXML/jackson-modules-java8) yet? – Ole V.V. Feb 01 '19 at 12:57
  • 1
    @DanielPatrick It should work if you register a `new JavaTimeModule()` - see here: http://fasterxml.github.io/jackson-datatype-jsr310/javadoc/2.6/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule.html – assylias Feb 01 '19 at 14:10

4 Answers4

11

You can custom a serializer and a deserializer for LocalDateTime, for exmaple:

CustomLocalDateTimeSerializer

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {

    protected CustomLocalDateTimeSerializer(Class<LocalDateTime> t) {
        super(t);
    }

    protected CustomLocalDateTimeSerializer() {
        this(null);
    }

    @Override
    public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider sp)
            throws IOException {
        Long epoch = value.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
        gen.writeString(epoch.toString());
    }
}

CustomLocalDateTimeDesSerializer:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

class CustomLocalDateTimeDesSerializer extends StdDeserializer<LocalDateTime> {

    protected CustomLocalDateTimeDesSerializer() {
        this(null);
    }

    protected CustomLocalDateTimeDesSerializer(Class<LocalDateTime> t) {
        super(t);
    }

    @Override
    public LocalDateTime deserialize(JsonParser jsonparser, DeserializationContext context)
            throws IOException {
        Long timestamp = Long.parseLong(jsonparser.getText());
        return LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
    }
}

And use the them:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.time.LocalDateTime;

public class RestObject {

    private LocalDateTime timestamp = LocalDateTime.now();

    @JsonSerialize(using = CustomLocalDateTimeSerializer.class)
    @JsonDeserialize(using = CustomLocalDateTimeDesSerializer.class)
    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        // {"timestamp":"1549026058"}
        System.out.println(objectMapper.writeValueAsString(new RestObject()));
    }
}
xingbin
  • 27,410
  • 9
  • 53
  • 103
  • 1
    This works... thank you! Can't believe Jackson doesn't have a configuration to allow the LocalDateTime serialization to be compatible with their old Date serialization. – Daniel Patrick Feb 01 '19 at 13:07
  • @DanielPatrick I'm not sure whether this is the most appropriate way, it's just a doable approach. – xingbin Feb 01 '19 at 13:14
8

The following solution solves the task of serialize/deserialise the LocalDateTime to the timestamp and relevant at least for spring-boot v1.5 and also includes next points that are not taken into account in the @xingbin's answer:

  • If there is a necessity to have the same behaviour as for java.util.Date have to use the toInstant().toEpochMilli() instead the toInstant().getEpochSecond()
  • Check on null the value to deserialize
  • And optional point: specify this serialization/deserialization configuration for the Jackson ObjectMapper

The timestamp serializer class:

public class LocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
    private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("UTC");

    public LocalDateTimeSerializer() {
        super(LocalDateTime.class);

    }

    @Override
    public void serialize(final LocalDateTime value,
                          final JsonGenerator generator,
                          final SerializerProvider provider) throws IOException {
        if (value != null) {
            final long mills = value.atZone(DEFAULT_ZONE_ID).toInstant().toEpochMilli();
            generator.writeNumber(mills);
        } else {
            generator.writeNull();
        }
    }
}

The timestamp deserializer class:

public class LocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
    private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("UTC");

    public LocalDateTimeDeserializer() {
        super(LocalDateTime.class);
    }

    @Override
    public LocalDateTime deserialize(final JsonParser parser,
                                     final DeserializationContext context) throws IOException {
        final long value = parser.getValueAsLong();
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(value), DEFAULT_ZONE_ID);
    }
}

The object mapper configuration:

@Configuration
public class ObjectMapperConfiguration {
    @Bean
    public ObjectMapper objectMapper() {
        final ObjectMapper objectMapper = new ObjectMapper();
        final SimpleModule module = new SimpleModule();
        module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        objectMapper.registerModule(module);
        return objectMapper;
    }
}
xxxception
  • 915
  • 10
  • 6
1

Here is how to do it simply and efficiently:

@JsonFormat(shape= JsonFormat.Shape.STRING, pattern="EEE MMM dd HH:mm:ss Z yyyy")
@JsonProperty("created_at") 
ZonedDateTime created_at;

This is a quote from a question: Jackson deserialize date from Twitter to `ZonedDateTime`, so this may be a duplicate question

Michael Gantman
  • 7,315
  • 2
  • 19
  • 36
0

Instead of rewriting everything manually, you could leverage the JavaTimeModule:

ObjectMapper om = new ObjectMapper();
om.registerModule(new JavaTimeModule());
om.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
assylias
  • 321,522
  • 82
  • 660
  • 783