8

I'm using Spring Boot 2.1.4 and Spring Data Jest with ElasticSearch. I was initially using Java Date for some properties with the following annotation:

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ")

This is saved into ElasticSearch as follows:

"creationDate": "2019-04-10T14:49:05.672+0000"

Now, I am in the process of migrating from Date to LocalDateTime and ZonedDateTime. When the data is now saved to ElasticSearch, I get the following attribute saved:

"creationDate": {
    "dayOfYear": 123,
    "dayOfWeek": "FRIDAY",
    "month": "MAY",
    "dayOfMonth": 3,
    "year": 2019,
    "monthValue": 5,
    "hour": 11,
    "minute": 54,
    "second": 12,
    "nano": 238000000,
    "chronology": {
        "id": "ISO",
        "calendarType": "iso8601"
    }
},

What do I need to do to change it so I get the same ElasticSearch data format as before for LocalDateTime and ZonedDateTime?

I have tried the following:

  1. Customising the object mapper as follows:

    public class CustomEntityMapper implements EntityMapper {
        private final ObjectMapper objectMapper;
    
        public CustomEntityMapper(ObjectMapper objectMapper) {  
            this.objectMapper = new ObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
            objectMapper.registerModule(new CustomGeoModule());
            objectMapper.registerModule(new JavaTimeModule());
        }
    
        @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);
        }
    }
    
  2. Adding the following to object mapper:

    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    

Any help or pointers where I'm going wrong would be appreciated.

Swordfish
  • 1,127
  • 24
  • 46
  • Never use `LocalDateTime` to represent a moment, as that class purposely lacks any cancer of time zone or offset-from-UTC. Use `Instant`, `OffsetDateTime`, or `ZonedDateTime` to represent a specific point on the timeline. – Basil Bourque May 06 '19 at 16:04
  • Thanks for your comment, why should LocalDateTime not be used? My example is a field for creationDate. – Swordfish May 06 '19 at 16:50
  • Your JSON input is representing a moment, a specific point on the timeline. A **`LocalDateTime` cannot represent a moment**. Read the class doc. Read many other postings on the subject here on Stack Overflow. – Basil Bourque May 06 '19 at 18:51
  • FYI, what you refer to as “ElasticSearch data format” is actually standard [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format for textually representing date-time values. These formats are used by default in *java.time* classes when parsing/generating strings. – Basil Bourque May 06 '19 at 18:53
  • Your example ISO 8601 string does not match your example JSON. When providing example data, be accurate. – Basil Bourque May 06 '19 at 18:54
  • Could you provide code sample of class (including annotations) you are trying to serialize to JSON? – Łukasz Gawron May 06 '19 at 20:08

3 Answers3

7

Managed to get it to work with Spring Boot 2.1.4 and Spring Data Jest. Here is what I did:

  1. Example domain object:

    @Document(indexName = "datetest")
    public class DateTest {
    
        @Id
        private String id;
    
        @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ", timezone = "UTC")
        private Instant instant = Instant.now();
    
        @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
        private ZonedDateTime zonedDateTime = ZonedDateTime.now();
    
        @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSS")
        private LocalDateTime localDateTime = LocalDateTime.now();
    
        // getters/setters
    }
    
  2. The ElasticSearch/JEST config:

    @Configuration
    public class ESConfig {
    
        @Bean
        public EntityMapper getEntityMapper() {
            return new CustomEntityMapper();
        }
    
        @Bean
        @Primary
        public ElasticsearchOperations elasticsearchTemplate(final JestClient jestClient,
                final ElasticsearchConverter elasticsearchConverter,
                final SimpleElasticsearchMappingContext simpleElasticsearchMappingContext, EntityMapper mapper) {
            return new JestElasticsearchTemplate(jestClient, elasticsearchConverter,
                    new DefaultJestResultsMapper(simpleElasticsearchMappingContext, mapper));
        }
    
        public class CustomEntityMapper implements EntityMapper {
    
            private final ObjectMapper objectMapper;
    
            public CustomEntityMapper() {
                objectMapper = new ObjectMapper();
                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
                objectMapper.registerModule(new CustomGeoModule());
                objectMapper.registerModule(new JavaTimeModule());
            }
    
            @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);
            }
    
        }
    }
    
  3. The results in ElasticSearch:

    Screenshot of results in ElasticSearch

Hope this helps.

g00glen00b
  • 41,995
  • 13
  • 95
  • 133
Swordfish
  • 1,127
  • 24
  • 46
3

That's because spring-data-jest uses DefaultEntityMapper (part of Spring Data), which creates its own ObjectMapper and doesn't use the one provided by Spring boot. This can be seen in this related question.

You're on the right track with your solution by defining your own EntityMapper, for example CustomEntityMapper. However, spring-data-jest wraps this mapper into a class called DefaultJestResultsMapper, which is then used by a bean called JestElasticsearchTemplate.

So, probably you should do something like this:

@Bean
public JestResultsMapper resultMapper(CustomEntityMapper entityMapper) {
    return new DefaultJestResultsMapper(entityMapper);
}

@Bean
public JestElasticSearchTemplate template(JestClient client, JestResultsMapper resultsMapper) {
    return new JestElasticSearchTemplate(client, resultsMapper);
}

This should inject your CustomEntityMapper into a JestResultsMapper, which is in turn injected into JestElasticSearchTemplate used by the framework.

Within CustomEntityMapper you can either autowire the default ObjectMapper (which will automatically add the JavaTimeModule) or you can configure one on your own.

g00glen00b
  • 41,995
  • 13
  • 95
  • 133
  • Hurrah! The above part was what I was missing. There were so many answers and different configurations so I stripped it back to a basic Spring Boot 2 test app and got it to work. I'll add my sample class and configuration above. Hope it helps other people, thanks for your support! – Swordfish May 07 '19 at 11:12
0

According to this answer from version 2 of Spring Boot,it should work out of the box as you want in terms of producing string from java.time objects

If you have

com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.4.0

as dependency and below line in application.properties

spring.jackson.serialization.write_dates_as_timestamps=false

So only thing left would be to add timezone notation to default string which won't have it.

If standard formatters won't work for you may always write your own serialiser/deserialiser and attach it like explained here

Łukasz Gawron
  • 897
  • 10
  • 20
  • 1
    That will work to configure the default `ObjectMapper`, but this mapper isn't used by the spring-data-jest framework, so this solution won't work. – g00glen00b May 07 '19 at 10:10
  • @g00glen00b thanks for claryfing that, I wasn't aware about that. I tried to help with gathering informations from other threads. – Łukasz Gawron May 07 '19 at 21:07