Search code examples
spring-bootjacksonspring-cloudjson-deserializationspring-cloud-feign

InvalidFormatException for Date - fixing without using JsonFormat or modifying original class


Introduction

We are using a custom starter hosted on a nexus repository, that contains spring-cloud-feign clients that make requests to microservices.

One of the microservices returns the dates as "dd-MM-yyyy HH:mm:ssZ" and this works in most of our applications. However, we have one application that is throwing the following error:

Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")

Current work-around

My Current work-around, as I don't want to pollute the starter, is to extend the class and create a local feign-client and local pojo with the proper JsonFormat:

public class DocumentMetaDataFix extends DocumentMetaData {
    @JsonFormat(
        shape = Shape.STRING,
        pattern = "yyyy-MM-dd HH:mm:ss"
    )
    private Date creationDate;
    @JsonFormat(
        shape = Shape.STRING,
        pattern = "yyyy-MM-dd HH:mm:ss"
    )

Failed Fixes

I have tried the following in my configuration class, in order to try affecting the de-serialization from another path. However, the DocumentMetaDataSerializer is never called. The ObjectMapper bean IS called.

  @Configuration
    @EnableSpringDataWebSupport
    @RequiredArgsConstructor
    public class MyConfig extends WebMvcConfigurerAdapter {

   @Bean
    public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
        return new Jackson2ObjectMapperBuilderCustomizer() {

            @Override
            public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
                jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
            }

        };
    }


    @Primary
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
        mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
        //mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
        return mapper;
    }

    @Bean
    public Module dynamoDemoEntityDeserializer() {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
        return module;
    }

    public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
        @Override
        public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            // return DynamoDemoEntity instance;

            JsonNode node = jp.getCodec().readTree(jp);


            return null;
        }

        public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
            JsonNode node = jp.getCodec().readTree(jp);


            return null;
        }

    }

Full Stacktrace

Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
 at [Source: (ByteArrayInputStream); line: 1, column: 580] (through reference chain: eu.europa.ec.nova.documentstore.DocumentMetaData["creationDate"])
        at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
        at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1548)
        at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:910)
        at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:524)
        at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:467)
        at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:195)
        at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:285)
        at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:268)
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
        ... 70 common frames omitted

So, any ideas? I have searched through the project for references to Jackson in case there is anything else in my project causing this.

I am will try to go inside the ObjectMapper and try to debug the current parameters/fields of the configuration at ObjectMapper.java:3084 from the stacktace:

at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 67 common frames omitted

Update

I added a breakpoint in the objectmapper constructor, and am seeing that it is being initialized from more than one location. This led me to suspect that spring-boot is not using my ObjectMapper. Instead it is using an internal spring one that is called from MappingJackson2HttpMessageConverter .

<init>:480, ObjectMapper
build:606, Jackson2ObjectMapperBuilder
<init>:59, MappingJackson2HttpMessageConverter
<init>:74, AllEncompassingFormHttpMessageConverter

I will therefore try to over-ride this internal spring one, based on results I found from: How to customise the Jackson JSON mapper implicitly used by Spring Boot?

However this also failed.

References

  1. Is it possible to configure Jackson custom deserializers at class level for different data types?
  2. https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper
  3. https://www.baeldung.com/jackson-deserialization
  4. very useful: https://mostafa-asg.github.io/post/customize-json-xml-spring-mvc-output/
  5. How to customise Jackson in Spring Boot 1.4

Update - final list of tries

It still fails with an error.

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.serializationInclusion(JsonInclude.Include.NON_NULL);
    builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
    builder.serializationInclusion(Include.NON_EMPTY);
    builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
    converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));

    //converters.add(cmsaMessageConverter());
    converters.add(new StringHttpMessageConverter());
    converters.add(new FormHttpMessageConverter());
    converters.add(new MappingJackson2HttpMessageConverter());
}


@Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
    return new Jackson2ObjectMapperBuilderCustomizer() {

        @Override
        public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
            jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
        }

    };
}


@Primary
@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
    //mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
    return mapper;
}

@Bean
public Module dynamoDemoEntityDeserializer() {
    SimpleModule module = new SimpleModule();
    module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
    return module;
}

public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
    @Override
    public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        // return DynamoDemoEntity instance;

        JsonNode node = jp.getCodec().readTree(jp);


        return null;
    }

    public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
        JsonNode node = jp.getCodec().readTree(jp);


        return null;
    }

}

It still fails with an error.


Solution

  • Try using LocalDateTime, this is what I'm doing and working for me

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime date;