4

We have a date field being populated with a long in elasticsearch index.

The field mapping is:

@Field(type = FieldType.Date)
@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
private LocalDateTime created;

And I use Jackson JavaTimeModule and Jdk8Module with this configuration:

@Bean
public ElasticsearchOperations elasticsearchTemplate() {
   return new ElasticsearchRestTemplate(client(), new CustomEntityMapper());
}

public static class CustomEntityMapper implements EntityMapper {

        private final ObjectMapper objectMapper;

        public CustomEntityMapper() {
            //we use this so that Elasticsearch understands LocalDate and LocalDateTime objects
            objectMapper = new ObjectMapper()
                              .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                              .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                              .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
                              .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                              .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
                              //MUST be registered BEFORE calling findAndRegisterModules
                              .registerModule(new JavaTimeModule())
                              .registerModule(new Jdk8Module());
            //only autodetect fields and ignore getters and setters for nonexistent fields when serializing/deserializing
            objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
                            .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                            .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                            .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
                            .withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
            //load the other available modules as well
            objectMapper.findAndRegisterModules();
        }

        @Override
        public String mapToString(Object object) throws IOException {
            return objectMapper.writeValueAsString(object);
        }

        @Override
        public <T> T mapToObject(String source, Class<T> clazz) throws IOException {
            return objectMapper.readValue(source, clazz);
        }
}

But when I try to parse an entity in the index with a field such as:

"created" : 1563448935000

I get an error:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (VALUE_NUMBER_INT), expected VALUE_STRING: Expected array or string.

I think, it is possible to deserialize a long to a date, but I don't see what I am missing.

If I map it to Long it works of course and same if the value is stored as String and we shape it and format properly in @JsonFormat. But is it possible to have long->LocalDateTime as well?

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
grog
  • 438
  • 1
  • 8
  • 21

3 Answers3

10

To build LocalDateTime from milliseconds from the epoch of 1970-01-01T00:00:00Z we need a time zone. In version 2.9.9 it throws exception when milliseconds appears:

raw timestamp (1563448935000) not allowed for java.time.LocalDateTime: need additional information such as an offset or time-zone (see class Javadocs)

But we can implement our deserialiser which will try to do this with default time zone. Example implementation could look like below:

class MillisOrLocalDateTimeDeserializer extends LocalDateTimeDeserializer {

    public MillisOrLocalDateTimeDeserializer() {
        super(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }

    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
            long value = parser.getValueAsLong();
            Instant instant = Instant.ofEpochMilli(value);

            return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
        }

        return super.deserialize(parser, context);
    }

}

ZoneOffset.UTC is used. In your case you can provide yours or use system default. Example usage:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        // override default
        javaTimeModule.addDeserializer(LocalDateTime.class, new MillisOrLocalDateTimeDeserializer());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(javaTimeModule);

        String json = "{\"created\":1563448935000}";
        System.out.println(mapper.readValue(json, Created.class));

    }
}

class Created {

    private LocalDateTime created;

    // getters, setters, toString
}

Above code prints:

Created{created=2019-07-18T11:22:15}

EDIT: Using Jackson 2.9.0, because of this issue the code provided will not be invoked since findAndRegisterModules which is called AFTER registering the customized module will override it. Removing that call will make the full scenario work. If above will not work for your version, you need to debug default implementation and find a reason.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Hello Michal, thank you for the answer. Yes I was wondering if I could avoid writing my own deserializer. We use UTC everywhere in our app and also Elasticsearch according to [docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html) uses UTC internally, I wanted to know whether I can pass additional parameters to Jackson itself maybe to make it use UTC when not specified or is using custom deserializer the only way? Consider we declare the field as **Date** on Elasticsearch and can therefore put anything in it – grog Jul 20 '19 at 10:44
  • 1
    @grog, to answer this question you need to take a look on source code for version of `Jackson` you use. When I started to write answer I was considering to configure it somehow but finally I had to take a look on implementation and what is possible. The newest version of `JavaTimeModule` [deserialiser](https://github.com/FasterXML/jackson-modules-java8/blob/master/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java) tells us we can only override it and implement some customisation in new class. To be continued... – Michał Ziober Jul 20 '19 at 14:07
  • 1
    @grog, There is no option to set time zone and use it from this deserialiser. You can of course implement this and create a new `JavaTimeModule` with such configuration possibilities but it takes time. If you think it is worth for you because you will use it in many projects I think it is worth to try. – Michał Ziober Jul 20 '19 at 14:10
  • 1
    Thank you Michal, I understand now. I will have to go with my own deserializer then or change the way we store the data. Have a nice day – grog Jul 22 '19 at 07:37
  • 1
    By the way, I edited your answer as such: this answer is correct and perfectly working on its own, I just point out that in the specific scenario described (using Jackson 2.9.0), because of [this](https://github.com/FasterXML/jackson-modules-java8/issues/122) the code provided will not be invoked since `findAndRegisterModules` which is called AFTER registering the customized module will override it. Removing that call will make the full scenario work. But it needs to be peer reviewed. If you could please update it yourself to reflect this it would be great. Thanks again and cheers – grog Jul 22 '19 at 11:47
  • @grog, thanks for and edit. I improved it a little bit. This is why I clearly notified about version `2.9.9`. Of course, clear info about other versions and that it can fail it could be helpful. – Michał Ziober Jul 22 '19 at 12:04
1

Use Instant as a Jackson field type for dates. This simplifies everything! All you will need is only register module: https://github.com/FasterXML/jackson-modules-java8

Sviatlana
  • 1,728
  • 6
  • 27
  • 55
0

I wanted to deserialize a Timestamp as a Long (fetched from a database record) to a ZonedDateTime and created a solution as shown below. Background is that I have a class Payment containing configuration properties in annotation @JsonFormat. I need to create a helper class for deserialization of the Long into a ZonedDateTime, this was class ZonedDateTimeDeserializer.java. In addition I am using a Web GUI so I will also go in the other direction, i.e. serialize a ZonedDateTime to a Long (which is saved to a database record), this is done in the helper class ZonedDateTimeSerializer.java.

Here is an extract from class Payment.java:

@JsonSerialize(using=ZonedDateTimeSerializer.class)
@JsonDeserialize(using=ZonedDateTimeDeserializer.class)
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss", timezone="Europe/Berlin")
protected ZonedDateTime timestamp;

Here is the helper classes:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

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

public class ZonedDateTimeDeserializer extends 
JsonDeserializer<ZonedDateTime> {

@Override
public ZonedDateTime deserialize(JsonParser jsonParser, 
    DeserializationContext context) throws IOException {
    long timestamp = jsonParser.getValueAsLong();
    Instant instant = Instant.ofEpochMilli(timestamp);
    return ZonedDateTime.ofInstant(instant, 
        ZoneId.of("Europe/Berlin"));
}
}

Here is the other helper class:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.time.ZonedDateTime;

public class ZonedDateTimeSerializer extends 
   JsonSerializer<ZonedDateTime> {

    @Override
    public void serialize(ZonedDateTime value, JsonGenerator gen, 
         SerializerProvider serializers) throws IOException {
        long epochMillis = value.toInstant().toEpochMilli();
        gen.writeNumber(epochMillis);
    }
}

Here is a JUnit test for the first helper class:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class ZonedDateTimeDeserializerTest {
     private ZonedDateTimeDeserializer deserializer;

     private JsonParser parser;
     private DeserializationContext ctxt;

     @BeforeEach
     public void setup() {
        deserializer = new ZonedDateTimeDeserializer();
        parser = mock(JsonParser.class);
        ctxt = mock(DeserializationContext.class);
     }

     @Test
     void testDeserialize() throws IOException {
         // Given
         long millis = 1681758192323L;
         when(parser.getValueAsLong()).thenReturn(millis);

         // When
         ZonedDateTime result = deserializer.deserialize(parser, 
     ctxt);

    // Then
    Instant instant = Instant.ofEpochMilli(millis);
    ZonedDateTime expected = ZonedDateTime.ofInstant(instant, 
       ZoneId.of("Europe/Berlin"));
    Assertions.assertThat(result).isEqualTo(expected);
  }
}

Here is a Junit-test for the second helper class:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

class ZonedDateTimeToLongSerializerTest {

    private ZonedDateTimeSerializer serializer;
    private JsonGenerator jsonGenerator;
    private SerializerProvider serializerProvider;

    @BeforeEach
    void setUp() {
       serializer = new ZonedDateTimeSerializer();
       jsonGenerator = mock(JsonGenerator.class);
       serializerProvider = mock(SerializerProvider.class);
    }

    @Test
    void testSerialize() throws IOException {
        // Given
        ZonedDateTime zonedDateTime = ZonedDateTime
            .ofInstant(Instants.ofLocalIso("2023-04-17T18:30:00"), 
      ZoneId.of("Europe/Berlin"));

    // When
    serializer.serialize(zonedDateTime, jsonGenerator, 
   serializerProvider);
// Then 
verify(jsonGenerator).writeNumber(zonedDateTime.toInstant().toEpochMilli());
 }
}
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 20 '23 at 08:53