QUESTION: Spring appears to use different deserialization methods for LocalDate
depending on whether it appears in a @RequestBody
or a request @R
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()));
}
}