Spring deserializes a LocalDate in a @RequestBody differently from one in a @RequestParam - why, and can they be the same?

java, java-8, json, serialization, spring

Solution

Per @Andreas in comments, the Spring Framework uses Jackson to deserialize `@RequestBody` but Spring itself deserializes `@RequestParam`. This is the source of the difference between the two.

This answer shows how to use `@ControllerAdvice` and `@InitBinder` to customize the deserialization of `@RequestParam`. The code I ultimately used follows:

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.text.Format;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;

@ControllerAdvice
public class ControllerAdviceInitBinder {

    private static class Editor<T> extends PropertyEditorSupport {

        private final Function<String, T> parser;
        private final Format format;

        public Editor(Function<String, T> parser, Format format) {

            this.parser = parser;
            this.format = format;
        }

        public void setAsText(String text) {

            setValue(this.parser.apply(text));
        }

        public String getAsText() {

            return format.format((T) getValue());
        }
    }

    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {

        webDataBinder.registerCustomEditor(
                Instant.class,
                new Editor<>(
                        Instant::parse,
                        DateTimeFormatter.ISO_INSTANT.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalDate.class,
                new Editor<>(
                        text -> LocalDate.parse(text, DateTimeFormatter.ISO_LOCAL_DATE),
                        DateTimeFormatter.ISO_LOCAL_DATE.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalDateTime.class,
                new Editor<>(
                        text -> LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                        DateTimeFormatter.ISO_LOCAL_DATE_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalTime.class,
                new Editor<>(
                        text -> LocalTime.parse(text, DateTimeFormatter.ISO_LOCAL_TIME),
                        DateTimeFormatter.ISO_LOCAL_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                OffsetDateTime.class,
                new Editor<>(
                        text -> OffsetDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
                        DateTimeFormatter.ISO_OFFSET_DATE_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                OffsetTime.class,
                new Editor<>(
                        text -> OffsetTime.parse(text, DateTimeFormatter.ISO_OFFSET_TIME),
                        DateTimeFormatter.ISO_OFFSET_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                ZonedDateTime.class,
                new Editor<>(
                        text -> ZonedDateTime.parse(text, DateTimeFormatter.ISO_ZONED_DATE_TIME),
                        DateTimeFormatter.ISO_ZONED_DATE_TIME.toFormat()));
    }
}

Problem

QUESTION: Spring appears to use different deserialization methods for `LocalDate` depending on whether it appears in a `@RequestBody` or a request `@ReqestParam` - is this correct, and if so, is there a way to configure them to be the same throughout an application? BACKGROUND: In my `@RestController`, I have two methods - one GET, and one POST. The GET expects a request parameter ("date") that is of type `LocalDate`; the POST expects a JSON object in which one key ("date") is of type `LocalDate`. Their signatures are similar to the following: ``` @RequestMapping(value = "/entity", method = RequestMethod.GET) public EntityResponse get( Principal principal, @RequestParam(name = "date", required = false) LocalDate date) @RequestMapping(value = "/entity", method = RequestMethod.POST) public EntityResponse post( Principal principal, @RequestBody EntityPost entityPost) public class EntityPost { public LocalDate date; } ``` I've configured my ObjectMapper as follows: ``` @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return objectMapper; } ``` Which ensures the system accepts `LocalDate` in the format yyyy-MM-dd and deserializes it as expected - at least when it is part of a `@RequestBody`. Thus if the following is the request body for the POST ``` { "date": 2017-01-01 } ``` The system deserializes the request body into an `EntityPost` as expected. However, that configuration does not apply to the deserialization of the `@RequestParam`. As a result, this fails: ``` // fail! /entity?date=2017-01-01 ``` Instead, the system appears to expect the format MM/dd/yy. As a result, this succeeds: ``` // success! /entity?date=01/01/17 ``` I know I can change this on a parameter-by-parameter basis using the @DateTimeFormat annotation. I know that if I change the signature of the GET method as follows, it will accept the first format: ``` @RequestMapping(value = "/entity", method = RequestMethod.GET) public EntityResponse get( Principal principal, @RequestParam(name = "date", required = false) @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate date) ``` However, I would prefer if I didn't have to include an annotation for every usage of `LocalDate`. Is there any way to set this globally, so that the system deserializes every `@RequestParam` of type `LocalDate` in the same way? For reference: I'm using Spring 4.3.2.RELEASE I'm using Jackson 2.6.5

Original source

Related problems