A few days ago I had to write a controller method that is used to upload files to a Spring web application.
This is an easy task, but I had another requirement that turned out to be a problem: every file has an expiration date that must be given when the file is uploaded.
What went wrong?
The Problem: Failed Type Conversion Attempt
The problem is that Spring cannot convert a request parameter string into an object that contains the date (and time) information. I will demonstrate this problem by using a simple controller that has two methods:
- The POST requests send to the url: '/api/datetime/date' are handled by the processDate() method.
- The POST requests send to the url: '/api/datetime/datetime' are handled by the processDateTime() method.
The source code of this controller class looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; import java.time.LocalDateTime; @RestController @RequestMapping("/api/datetime/") final class DateTimeController { @RequestMapping(value = "date", method = RequestMethod.POST) public void processDate(@RequestParam("date") LocalDate date) { //Do stuff } @RequestMapping(value = "datetime", method = RequestMethod.POST) public void processDateTime(@RequestParam("datetime") LocalDateTime dateAndTime) { //Do stuff } }
This controller class has two problems:
- When we send a POST request to the url: '/api/datetime/date' and set the value of the date request parameter to: 2015-09-26 (ISO 8601 date format), a ConversionFailedException is thrown.
- When we send a POST request to the url: '/api/datetime/datetime' and set the value of the datetime request parameter to: 2015-09-26T01:30:00.000 (ISO 8601 date and time format), a ConversionFailedException is thrown.
In other words, we cannot use the API provided by the DateTimeController class because it is broken. Let's find out how we can fix it.
@DateTimeFormat Annotation to the Rescue
The Javadoc of the @DateTimeFormat annotation states that it declares that a field should be formatted as a date time. Even though the Javadoc doesn't mention it, we can use this annotation for specifying the pattern that is used by our request parameters.
These examples demonstrate how we can use this annotation:
Example 1:
If we want to use the ISO 8601 date format (yyyy-MM-dd), we have to annotate the controller method parameter with the @DateTimeFormat annotation and set the value of its iso attribute to DateTimeFormat.ISO.DATE. The controller class that uses this date format looks as follows:
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; @RestController @RequestMapping("/api/datetime/") final class DateTimeController { @RequestMapping(value = "date", method = RequestMethod.POST) public void processDate(@RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { //Do stuff } }
Example 2:
If we want to use a custom date format (like the Finnish date format: dd.MM.yyyy), we have to annotate the controller method parameter with the @DateTimeFormat annotation and set the value of its pattern attribute to 'dd.MM.yyyy'. The controller class that uses this date format looks as follows:
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; @RestController @RequestMapping("/api/datetime/") final class DateTimeController { @RequestMapping(value = "date", method = RequestMethod.POST) public void processDate(@RequestParam("date") @DateTimeFormat(pattern = "dd.MM.yyyy") LocalDate date) { //Do stuff } }Example 3:
If we want to use the ISO 8601 date and time format (yyyy-MM-dd'T'HH:mm:ss.SSSZ), we have to annotate the controller method parameter with the @DateTimeFormat annotation and set the value of its iso attribute to DateTimeFormat.ISO.DATE_TIME. The controller class that uses this date format looks as follows:
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; @RestController @RequestMapping("/api/datetime/") final class DateTimeController { @RequestMapping(value = "datetime", method = RequestMethod.POST) public void processDateTime(@RequestParam("datetime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateAndTime) { //Do stuff } }
Example 4:
If we want to use a custom date and time format (like the Finnish date and time format: dd.MM.yyyy HH:mm:ss.SSSZ), we have to annotate the controller method parameter with the @DateTimeFormat annotation and set the value of its pattern attribute to ‘dd.MM.yyyy HH:mm:ss.SSSZ’. The controller class that uses this date format looks as follows:
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; @RestController @RequestMapping("/api/datetime/") final class DateTimeController { @RequestMapping(value = "datetime", method = RequestMethod.POST) public void processDateTime(@RequestParam("datetime") @DateTimeFormat(pattern = "dd.MM.yyyy HH:mm:ss.SSSZ") LocalDateTime dateAndTime) { //Do stuff } }
Let's summarize what we learned from this blog post.
Summary
This blog post has taught us three things:
- We can apply the @DateTimeFormat annotation to java.util.Date, java.util.Calendar, java.lang.Long, Joda-Time value types; and as of Spring 4 and JDK 8, to JSR-310 java.time types too.
- If we want to use the ISO 8601 format, we have to configure the used format by setting the value of the @DateTimeFormat annotation's iso attribute.
- If we want to use a custom format, we have to configure the used format by setting the value of the @DateTimeFormat annotation's pattern attribute.
P.S. You can get the example application of this blog post from Github.
Is there a global mechanism to regiregister Date and DateTime conversion for parameters? For Spring standard binding , like there is on Jackson object mapper? I'm thinking both on post and get (uri parameters)
Yes. You can use a type converter. I will write a new blog post about this, but you can get started by reading this StackOverflow question.
Thanks Petri. I remember using String and date formatter for this task but this is a clean way of doing it.
You are welcome! However, if you need to process date and time information in multiple controllers, you might want to use type converters -> you don't have to remember to annotate the relevant method parameters with the
@DateTimeFormat
annotation.In Spring how would you handle an exception thrown if the date format sent by the client was a bad date? For example I want to return a custom message as JSON stating that the Date string sent is invalid.
There are many ways to handle exceptions with Spring MVC. However, I think that the best way to fulfill your requirement is to create an exception handler method.
Petri,
Thanks for your contribution, as always.
When using the @DateTimeFormat, as you used it above, I still get parsing errors:
ConversionFailedException: Failed to convert from type [java.lang.String] to type [@io.swagger.annotations.ApiParam @org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.time.LocalDate] for value '8/19/2016'; nested exception is java.time.format.DateTimeParseException: Text '8/19/2016' could not be parsed at index 0"
My controller parameter:
@RequestParam @DateTimeFormat(pattern = "MM/dd/yyyy") LocalDate startDate
The input is simply "8/19/2016". Any thoughts.
Hi,
The problem might be that the month has only one digit (e.g. '8' instead of '08'). Have you tried using the pattern: 'M/d/yyyy'?
How can you customize the conversion failed error message?
I've been stuck on this since Wednesday, THANK YOU SO SO SO SO SO SO SO SO SO MUCH!
You are welcome. I am happy to hear that this blog post was useful to you.
Unfortunately, this is broken in Spring boot 1.3.6 + Spring Cloud Brixton.SR3
https://github.com/spring-cloud/spring-cloud-netflix/issues/1178
It seems so :(
Thanks. This hit the spot.
You are welcome! I am happy to hear that this blog post was useful to you.
Hi Petri,
What if my json request has one property which is date like '2017-08-29' and i want customer with registration date to be set to entity ( which is string ); how it should be handled?
Hi,
You can simply add a
LocalDate
field to your DTO and the JacksonObjectMapper
will transform theString
object into aLocalDate
object (and vice versa).Just what I needed. Thanks!
You are welcome!
How to handle null date?
Hi,
The
@RequestParam
annotation has an attribute calledrequired
. This attribute is used to define whether the request is required or not. Because the default value of this attribute istrue
, you have to change the value of this attribute tofalse
.This allows you to handle the situations when the request parameter is not given or when its value is an empty string (in both cases, the method parameter of your controller method will be
null
).Hi Petri,
How do you use this if the LocalDate field is inside a request DTO? I tried putting @DateTimeFormat in the DTO's constructor, on the field, and even in the controller method parameter, but none have worked.
Hi Raymond,
You can either change the configuration of your
ObjectMapper
or you can use the@JsonFormat
annotation. Take a look at this blog post. If you are using Spring Boot, you should take a look at its reference manual.Hi Petri,
Thank you for the quick response. Turns out the module wasn't registered with the ObjectMapper all this time.
Cheers!
You are welcome! Also, it's good to hear that you were able to solve your problem.
My controller is @DateTimeFormat( iso = DateTimeFormat.ISO.Date) LocalDate startDate when send request as startDate as 2017-08-08T00:00:00.000Z or as 2017/08/08 I'm getting failed to convert from type string to type LocalDate
Hi,
If you use the
DateTimeFormat.ISO.Date
, your request parameter must use the format:yyyy-MM-dd
.Appreciate the link, I've always set the param as a string and then converted it myself, this seems a better way to enforce the contract within the method signature. And if you do want to keep the Zone, here's how you would do that: https://stackoverflow.com/questions/34398387/zoneddatetime-as-pathvariable-in-spring-rest-requestmapping
paraphrasing here, but the idea is to use a custom converter.
@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) ZonedDateTime startDate
how does use custom error messages in Spring boot with thymeleaf? I'm trying but just appears the error page so I get stuck now for 3 days lol
Do you want to display one custom error page or use different error pages for different errors?
Is it possible to provide default value as current date ?
Yes. Take a look at this StackOverflow answer. Unfortunately, the solution is "a bit ugly", but it solves your problem.
Thank you very much. this post same my day. Keep the good work. From Cabo Verde Islands