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
Advertisement
Answer
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())); } }