Search code examples
springspring-bootjacksonspring-restcontrollerjackson2

Use Jackson Deserialization for DateTime in Spring REST Controller


I am getting this exception when trying to deserialize the value passed on the POST request to the Spring controller from String to OffsetDateTime.

Here is my exception:

Failed to convert value of type 'java.lang.String' to required type 'java.time.OffsetDateTime';
    nested exception is org.springframework.core.convert.ConversionFailedException: 
        Failed to convert from type [java.lang.String] to 
           type [@org.springframework.web.bind.annotation.RequestParam java.time.OffsetDateTime]
           for value '2018-03-02T14:12:50.789+01:00';
           nested exception is java.lang.IllegalArgumentException:
                Parse attempt failed for value [2018-03-02T14:12:50.789+01:00]

I am using the latest version of Spring-Boot - 2.0.1.BUILD-SNAPSHOT

Here is my JacksonConfig.java

package com.divinedragon.jackson.config;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.fasterxml.jackson.databind.DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL;
import static com.fasterxml.jackson.databind.PropertyNamingStrategy.SNAKE_CASE;
import static com.fasterxml.jackson.databind.SerializationFeature.WRAP_ROOT_VALUE;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;

@Configuration
public class JacksonConfig {

    @Bean(name = "jacksonConverter")
    public MappingJackson2HttpMessageConverter jacksonConverter(final ObjectMapper objectMapper) {

        final MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();

        httpMessageConverter.setObjectMapper(objectMapper);

        return httpMessageConverter;
    }

    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        final ObjectMapper mapper = new ObjectMapper();

        mapper.enable(READ_UNKNOWN_ENUM_VALUES_AS_NULL);

        mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES);
        mapper.disable(WRAP_ROOT_VALUE);

        mapper.setDateFormat(new ISO8601DateFormat());
        mapper.setPropertyNamingStrategy(SNAKE_CASE);

        mapper.registerModule(new Jdk8Module());
        mapper.registerModule(new JavaTimeModule());
        mapper.registerModule(new ParameterNamesModule());

        return mapper;
    }
}

And here is my JacksonController.java which is a Spring REST Controller.

package com.divinedragon.jackson.controller;

import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JacksonController {

    @GetMapping(path = "/get")
    public Map<String, OffsetDateTime> getDates() {
        return Collections.singletonMap("createdAt", OffsetDateTime.now());
    }

    @PostMapping(path = "/post")
    public Map<String, OffsetDateTime> postDates(@RequestParam("created_at") final OffsetDateTime createdAt) {
        return Collections.singletonMap("createdAt", createdAt);
    }
}

This application runs and when I make request to the /get end-point, I get the date value serialized using Jackson correctly.

-> curl -s http://localhost:8080/get | python -m json.tool
{
    "createdAt": "2018-03-02T14:12:50.789+01:00"
}

When I use the /post end-point and pass the date value, I am getting the above exception:

-> curl -s -X POST http://localhost:8080/post --data-urlencode 'created_at=2018-03-02T14:12:50.789+01:00' | python -m json.tool
{
    "error": "Bad Request",
    "message": "Failed to convert value of type 'java.lang.String' to required type 'java.time.OffsetDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.time.OffsetDateTime] for value '2018-03-02T14:12:50.789+01:00'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2018-03-02T14:12:50.789+01:00]",
    "path": "/post",
    "status": 400,
    "timestamp": "2018-03-02T13:15:38Z"
}

Can somebody guide me as to how can I have the Jackson deserialization be used for converting the values to OffsetDateTime?


Solution

  • Seems like there is no way to have Jackson intercept conversion of String to OffsetDateTime passed via @RequestParam.

    To make this work, I ended up writing my own Converter with the help of post here.

    Here is my converter:

    @Component
    public class CustomOffsetDateTimeConverter implements Converter<String, OffsetDateTime> {
    
        @Autowired
        private DateTimeFormatter dateTimeFormatter;
    
        @Override
        public OffsetDateTime convert(final String source) {
            return OffsetDateTime.parse(source, dateTimeFormatter);
        }
    
    }
    

    Also, so as to have Jackson also conform to the same DateTimeFormat, I updated my Jackson configuration.

    A quick thing which I got to know is that if you want to update the format of Serialization/Deserialization, this doesn't work.

    objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
    

    So, I ended up writing custom Serializer/Deserializer for this purpose and then overwritten the defaults from JavaTimeModule.

    Here is my updated JacksonConfig.java

    @Configuration
    public class JacksonConfig {
    
        @Bean("dateTimeFormatter")
        public DateTimeFormatter dateTimeFormatter() {
            return DateTimeFormatter.ofPattern(DATE_TIME_FORMAT_STRING);
        }
    
        @Bean
        @Primary
        public ObjectMapper objectMapper(final DateTimeFormatter dateTimeFormatter) {
            final ObjectMapper mapper = new ObjectMapper();
    
            mapper.enable(READ_UNKNOWN_ENUM_VALUES_AS_NULL);
    
            mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES);
            mapper.disable(WRAP_ROOT_VALUE);
            mapper.disable(WRITE_DATES_AS_TIMESTAMPS);
    
            mapper.setPropertyNamingStrategy(SNAKE_CASE);
    
            final JavaTimeModule javaTimeModule = new JavaTimeModule();
            javaTimeModule.addSerializer(OffsetDateTime.class, new CustomOffsetDateTimeSerializer(dateTimeFormatter));
            javaTimeModule.addDeserializer(OffsetDateTime.class, new CustomOffsetDateTimeDeserializer(dateTimeFormatter));
    
            mapper.registerModule(new Jdk8Module());
            mapper.registerModule(javaTimeModule);
            mapper.registerModule(new ParameterNamesModule());
    
            return mapper;
        }
    
        @Bean(name = "jacksonConverter")
        public MappingJackson2HttpMessageConverter jacksonConverter(final ObjectMapper objectMapper) {
    
            final MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
    
            httpMessageConverter.setObjectMapper(objectMapper);
    
            return httpMessageConverter;
        }
    }
    
    class CustomOffsetDateTimeSerializer extends JsonSerializer<OffsetDateTime> {
    
        private final DateTimeFormatter dateTimeFormatter;
    
        public CustomOffsetDateTimeSerializer(final DateTimeFormatter dateTimeFormatter) {
            this.dateTimeFormatter = dateTimeFormatter;
        }
    
        @Override
        public void serialize(final OffsetDateTime value, final JsonGenerator gen, final SerializerProvider serializers)
                throws IOException {
            gen.writeString(dateTimeFormatter.format(value));
        }
    
    }
    
    @Component
    class CustomOffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {
    
        private final DateTimeFormatter dateTimeFormatter;
    
        public CustomOffsetDateTimeDeserializer(final DateTimeFormatter dateTimeFormatter) {
            this.dateTimeFormatter = dateTimeFormatter;
        }
    
        @Override
        public OffsetDateTime deserialize(final JsonParser p, final DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            return OffsetDateTime.parse(p.getValueAsString(), dateTimeFormatter);
        }
    }
    

    Hope this helps somebody, someday.