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
Can we use RequestBody and RequestParam together? nothing. So it fails with 400 because the request can't be correctly handled by the handler method.
By using @RequestBody annotation you will get your values mapped with the model you created in your system for handling any specific call. While by using @ResponseBody you can send anything back to the place from where the request was generated. Both things will be mapped easily without writing any custom parser etc.
Simply put, the @RequestBody annotation maps the HttpRequest body to a transfer or domain object, enabling automatic deserialization of the inbound HttpRequest body onto a Java object. Spring automatically deserializes the JSON into a Java type, assuming an appropriate one is specified.
HTTP's GET method does not include a request body as part of the spec. Spring MVC respects the HTTP specs. Specifically, servers are allowed to discard the body.
Create a Formatter for LocalDate:
public class LocalDateFormatter implements Formatter<LocalDate> {
@Override
public LocalDate parse(String text, Locale locale) throws ParseException {
return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
}
@Override
public String print(LocalDate object, Locale locale) {
return DateTimeFormatter.ISO_DATE.format(object);
}
}
Spring 5+:
Register the formatter:
Implement WebMvcConfigurer
in your @Configuration
and override addFormatters
:
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateFormatter());
}
Spring Boot: Define a @Primary @Bean
to override the default formatter:
@Bean
@Primary
public Formatter<LocalDate> localDateFormatter() {
return new LocalDateFormatter();
}
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()));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With