I’m having trouble integrating Spring with Jackson.
I have a POJO which has some Instant
fields with a custom date format:
public class C{
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss", timezone = "UTC")
private Instant postedAtFrom;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss", timezone = "UTC")
private Instant postedAtTo;
}
Then I’m trying to use this POJO as a query parameter for a rest GET mapping:
@RestController
@RequestMapping("/c")
@RequiredArgsConstructor
@Slf4j
public class AnnounceController {
@Autowired
private final ObjectMapper objectMapper;
@PostConstruct
public void test() throws JsonProcessingException {
System.out.println(objectMapper.readValue("{"postedAtFrom":"20-05-2022 15:50:07"}", C.class));
}
@GetMapping
public BasicResponse<Page<Announce>> get(final C filter, @NonNull final Pageable page) {
return new BasicResponse<>(service.get(filter, page));
}
}
The PostConstruct
code works fine and it prints exactly what it should print, but whenever I try to make a get to that endpoint with the very same request:
CURL
GET /c?postedAtFrom=20-05-2022 15:50:07 HTTP/1.1
Host: localhost
I get this exception:
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'filter' on field 'postedAtFrom': rejected value [20-05-2022 15:50:07]
codes [typeMismatch.filter.postedAtFrom,typeMismatch.postedAtFrom,typeMismatch.java.time.Instant,typeMismatch]
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [filter.postedAtFrom,postedAtFrom]
arguments []
default message [postedAtFrom]]
default message [Failed to convert property value of type 'java.lang.String' to required type 'java.time.Instant' for property 'postedAtFrom'
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonFormat java.time.Instant] for value [20-05-2022 15:50:07]
nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [20-05-2022 15:50:07]]
Here is the code for the ObjectMapper
bean:
@Bean
@Primary
public ObjectMapper configure(@NonNull final ObjectMapper objectMapper) {
final SimpleModule javaTimeModule = new JavaTimeModule()
.addDeserializer(Instant.class, InstantDeserializer.INSTANT)
.addSerializer(Instant.class, InstantSerializer.INSTANCE);
return objectMapper.registerModule(javaTimeModule);
}
I would like to to resolve this using the designed tools of Spring and Jackson. Please do not suggest a workaround like “convert it to a string” or “make a post”.
8
As others have stated in the comments, it’s not very clear what you’re expecting to happen. I make assumptions below.
Considering the @JsonFormat
annotation and the custom ObjectMapper
, if you’re expecting Jackson to kick in here and parse JSON, that’s not going to happen. To be clear, no Jackson nor JSON parsing is involved anywhere in here.
Your final C filter
handler method parameter isn’t annotated with something like @RequestBody
(and you’re not sending a request body in that GET Curl call). Without an annotation, Spring MVC falls back to handling the parameter as a model attribute and processes it with ModelAttributeMethodProcessor
.
It creates an instance of your type C
and attempts to populate its fields with query (or form- if that’s what you had used) parameters. In this case, it’ll look for a query parameter named postedAtFrom
to match the field (really the bean/pojo accessor, which I also assume you have).
The processor uses a set of Converter instances to populate the fields from String values (the query parameters) into the complex types your model attribute type is likely using.
Spring Boot maintains its own list of default Converter
instances. There’s one in there for java.time
types, Jsr310DateTimeFormatAnnotationFormatterFactory
(this isn’t the Converter
, but it’s what the Converter
uses), which is set up to handle String
to Instant
conversion.
However, your query parameter value, 20-05-2022 15:50:07
, is not going to be parseable as an Instant
, there’s information about the Instant
missing (timezone for one). In certain, you’d be able to annotate your field @DateTimeFormat
to specify a custom format, but I don’t think that’ll work here.
You can instead change your field to be a LocalDateTime
@DateTimeFormat(pattern = "dd-MM-yyyy HH:mm:ss")
private LocalDateTime postedAtFrom;
with an appropriate pattern matching your input. And, whenever you need an Instant
, use LocalDateTime
‘s toInstant
method with an appropriate ZoneOffset
.
Alternatively, you can configure Spring Boot with your own list of Converter
instances that can parse directly to an Instant
with your own DateTimeFormatter
. See this post for information on how to do that.
1
@Savior’s answer looked very good but because I didn’t really know how a new date converter would mingle among the others spring already has I went to a less elegant but easier solution. I added a set method in my POJO
@DateTimeFormat(pattern = "dd-MM-yyyy HH:mm:ss")
public void setPostedAtFrom(final String date) {
this.postedAtFrom = LocalDateTime.parse(date, SIMPLE_DATE_FORMAT).atZone(ZoneId.systemDefault()).toInstant();
}
@DateTimeFormat(pattern = "dd-MM-yyyy HH:mm:ss")
public void setPostedAtTo(final String date) {
this.postedAtTo = LocalDateTime.parse(date, SIMPLE_DATE_FORMAT).atZone(ZoneId.systemDefault()).toInstant();
}
private static final DateTimeFormatter SIMPLE_DATE_FORMAT = new DateTimeFormatterBuilder()
.appendPattern("dd-MM-yyyy HH:mm:ss")
.toFormatter();
the @DateTimeFormat
is really irrelevant here but helps me to remember what is the format that string is supposed to come [this is a really big pojo]
1