How to use LocalDateTime RequestParam in Spring? I get "Failed to convert String to LocalDateTime"
Asked Answered
P

17

144

I use Spring Boot and included jackson-datatype-jsr310 with Maven:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.7.3</version>
</dependency>

When I try to use a RequestParam with a Java 8 Date/Time type,

@GetMapping("/test")
public Page<User> get(
    @RequestParam(value = "start", required = false)
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start) {
//...
}

and test it with this URL:

/test?start=2016-10-8T00:00

I get the following error:

{
  "timestamp": 1477528408379,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
  "message": "Failed to convert value of type [java.lang.String] to required type [java.time.LocalDateTime]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.time.LocalDateTime] for value '2016-10-8T00:00'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2016-10-8T00:00]",
  "path": "/test"
}
Perutz answered 27/10, 2016 at 0:46 Comment(0)
E
134

TL;DR - you can capture it as a string with just @RequestParam, or you can have Spring additionally parse the string into a java date / time class via @DateTimeFormat on the parameter as well.

The @RequestParam is enough to grab the date you supply after the = sign. However, it comes into the method as a String. That is why it is throwing the cast exception.

There are a few ways to achieve this:

  1. Parse the date yourself, grabbing the value as a string.
@GetMapping("/test")
public Page<User> get(@RequestParam(value="start", required = false) String start){

    //Create a DateTimeFormatter with your required format:
    DateTimeFormatter dateTimeFormat = 
            new DateTimeFormatter(DateTimeFormatter.BASIC_ISO_DATE);

    //Next parse the date from the @RequestParam, specifying the TO type as a TemporalQuery:
   LocalDateTime date = dateTimeFormat.parse(start, LocalDateTime::from);
    
    //Do the rest of your code...
}
  1. Leverage Spring's ability to automatically parse and expect date formats:
@GetMapping("/test")
public void processDateTime(@RequestParam("start") 
                            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) 
                            LocalDateTime date) {
        // The rest of your code (Spring already parsed the date).
}
Example answered 27/10, 2016 at 4:57 Comment(4)
Sure, but there is one major problem - why use custom controller, if for most of those requests You could use Spring JPA Repositories? And this is the place when actually problem with this error starts ;/Encumbrance
You could also use this solution in the signature method: @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startSynopsize
@Synopsize please post your comment as an answer as it should be the accepted one imoEgin
Thank-you, approach 2 works for me as sometimes i pass minuets, and other times i don't need too. this just takes care of all that :)Booboo
G
99

You did everything correct :) . Here is an example that shows exactly what you are doing. Just Annotate your RequestParam with @DateTimeFormat. There is no need for special GenericConversionService or manual conversion in the controller. This blog post writes about it.

@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
    }
}

I guess you had an issue with the format. On my setup everything works well.

Glasser answered 2/10, 2017 at 18:14 Comment(5)
I went with this advice, and it worked, but then I wondered if the annotation could be applied to the entire controller method...and it turns out it can. It cannot be applied to the entire controller, though: @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface DateTimeFormat {.Beane
Notwithstanding my comment above, moving the annotation from a request parameter (two of them, actually: startDate and endDate) to the request method seemed to break the method's behavior for the worse.Beane
This works well for date patterns that don't have timestamps, but if you include a timestamp in the pattern, it fails to convert the String to a Date (or other applicable type).Stodgy
I was wrong, this works just fine with timestamps, but if you copy-paste the example that is in the JavaDoc for org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME, it'll fail. The example that they provide should have X instead of Z for its pattern as they included -05:00 as opposed to -0500.Stodgy
I tried this solution and it works if you pass date or DateTime, but when the values are EMPTY, this is failing.Payton
S
48

I found workaround here.

Spring/Spring Boot only supports the date/date-time format in BODY parameters.

The following configuration class adds support for date/date-time in QUERY STRING (request parameters):

// Since Spring Framwork 5.0 & Java 8+
@Configuration
public class DateTimeFormatConfiguration implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

respectively:

// Until Spring Framwork 4.+
@Configuration
public class DateTimeFormatConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

It works even if you bind multiple request parameters to some class (@DateTimeFormat annotation helpless in this case):

public class ReportRequest {
    private LocalDate from;
    private LocalDate to;

    public LocalDate getFrom() {
        return from;
    }

    public void setFrom(LocalDate from) {
        this.from = from;
    }

    public LocalDate getTo() {
        return to;
    }

    public void setTo(LocalDate to) {
        this.to = to;
    }
}

// ...

@GetMapping("/api/report")
public void getReport(ReportRequest request) {
// ...
Superfecundation answered 21/12, 2017 at 23:36 Comment(3)
how to catch convert exception here?Adiell
This is the best answer. It works even if the Date field is a nested field. It's also better because this way you just have to add this config once.Advection
Whoa! Trawled through half the internet to get here... Thank you!Gymnasiast
S
24

Like I put in the comment, you could also use this solution in the signature method: @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start

Synopsize answered 30/10, 2017 at 9:56 Comment(0)
A
17

SpringBoot 2.X.X and newer

If you use the dependency spring-boot-starter-web version 2.0.0.RELEASE or higher, there is no longer needed to explicitely include jackson-datatype-jsr310 dependency, which is already provided with spring-boot-starter-web through spring-boot-starter-json.

This was resolved as Spring Boot issue #9297 and the answer is still valid and relevant:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
@RequestMapping(value = "datetime", method = RequestMethod.POST)
public void foo(
        @RequestParam("dateTime") 
        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime ldt) {

    // IMPLEMENTATION
}
Allocution answered 2/7, 2020 at 14:30 Comment(0)
T
4

I ran into the same problem and found my solution here (without using Annotations)

...you must at least properly register a string to [LocalDateTime] Converter in your context, so that Spring can use it to automatically do this for you every time you give a String as input and expect a [LocalDateTime]. (A big number of converters are already implemented by Spring and contained in the core.convert.support package, but none involves a [LocalDateTime] conversion)

So in your case you would do this:

public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
    public LocalDateTime convert(String source) {
        DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;
        return LocalDateTime.parse(source, formatter);
    }
}

and then just register your bean:

<bean class="com.mycompany.mypackage.StringToLocalDateTimeConverter"/>

With Annotations

add it to your ConversionService:

@Component
public class SomeAmazingConversionService extends GenericConversionService {

    public SomeAmazingConversionService() {
        addConverter(new StringToLocalDateTimeConverter());
    }

}

and finally you would then @Autowire in your ConversionService:

@Autowired
private SomeAmazingConversionService someAmazingConversionService;

You can read more about conversions with spring (and formatting) on this site. Be forewarned it has a ton of ads, but I definitely found it to be a useful site and a good intro to the topic.

Telford answered 3/12, 2016 at 23:6 Comment(0)
A
4

Following works well with Spring Boot 2.1.6:

Controller

@Slf4j
@RestController
public class RequestController {

    @GetMapping
    public String test(RequestParameter param) {
        log.info("Called services with parameter: " + param);
        LocalDateTime dateTime = param.getCreated().plus(10, ChronoUnit.YEARS);
        LocalDate date = param.getCreatedDate().plus(10, ChronoUnit.YEARS);

        String result = "DATE_TIME: " + dateTime + "<br /> DATE: " + date;
        return result;
    }

    @PostMapping
    public LocalDate post(@RequestBody PostBody body) {
        log.info("Posted body: " + body);
        return body.getDate().plus(10, ChronoUnit.YEARS);
    }
}

Dto classes:

@Value
public class RequestParameter {
    @DateTimeFormat(iso = DATE_TIME)
    LocalDateTime created;

    @DateTimeFormat(iso = DATE)
    LocalDate createdDate;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostBody {
    LocalDate date;
}

Test class:

@RunWith(SpringRunner.class)
@WebMvcTest(RequestController.class)
public class RequestControllerTest {

    @Autowired MockMvc mvc;
    @Autowired ObjectMapper mapper;

    @Test
    public void testWsCall() throws Exception {
        String pDate        = "2019-05-01";
        String pDateTime    = pDate + "T23:10:01";
        String eDateTime = "2029-05-01T23:10:01"; 

        MvcResult result = mvc.perform(MockMvcRequestBuilders.get("")
            .param("created", pDateTime)
            .param("createdDate", pDate))
          .andExpect(status().isOk())
          .andReturn();

        String payload = result.getResponse().getContentAsString();
        assertThat(payload).contains(eDateTime);
    }

    @Test
    public void testMapper() throws Exception {
        String pDate        = "2019-05-01";
        String eDate        = "2029-05-01";
        String pDateTime    = pDate + "T23:10:01";
        String eDateTime    = eDate + "T23:10:01"; 

        MvcResult result = mvc.perform(MockMvcRequestBuilders.get("")
            .param("created", pDateTime)
            .param("createdDate", pDate)
        )
        .andExpect(status().isOk())
        .andReturn();

        String payload = result.getResponse().getContentAsString();
        assertThat(payload).contains(eDate).contains(eDateTime);
    }


    @Test
    public void testPost() throws Exception {
        LocalDate testDate = LocalDate.of(2015, Month.JANUARY, 1);

        PostBody body = PostBody.builder().date(testDate).build();
        String request = mapper.writeValueAsString(body);

        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("")
            .content(request).contentType(APPLICATION_JSON_VALUE)
        )
        .andExpect(status().isOk())
        .andReturn();

        ObjectReader reader = mapper.reader().forType(LocalDate.class);
        LocalDate payload = reader.readValue(result.getResponse().getContentAsString());
        assertThat(payload).isEqualTo(testDate.plus(10, ChronoUnit.YEARS));
    }

}
Abney answered 12/7, 2019 at 8:29 Comment(0)
B
3

The answers above didn't work for me, but I blundered on to one which did here: https://blog.codecentric.de/en/2017/08/parsing-of-localdate-query-parameters-in-spring-boot/ The winning snippet was the ControllerAdvice annotation, which has the advantage of applying this fix across all your controllers:

@ControllerAdvice
public class LocalDateTimeControllerAdvice
{

    @InitBinder
    public void initBinder( WebDataBinder binder )
    {
        binder.registerCustomEditor( LocalDateTime.class, new PropertyEditorSupport()
        {
            @Override
            public void setAsText( String text ) throws IllegalArgumentException
            {
                LocalDateTime.parse( text, DateTimeFormatter.ISO_DATE_TIME );
            }
        } );
    }
}
Bocage answered 20/7, 2019 at 22:39 Comment(0)
E
3

You can global configure datetime format in application properties. Like:

spring.mvc.format.date=yyyy-MM-dd

spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss

spring.mvc.format.time=HH:mm:ss

Check in mavern: org.springframework.boot:spring-boot-autoconfigure:2.5.3

Efficient answered 3/8, 2021 at 13:59 Comment(0)
B
1

You can add to config, this solution does work with optional as well as with non-optional parameters.

@Bean
    public Formatter<LocalDate> localDateFormatter() {
        return new Formatter<>() {
            @Override
            public LocalDate parse(String text, Locale locale) {
                return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
            }

            @Override
            public String print(LocalDate object, Locale locale) {
                return DateTimeFormatter.ISO_DATE.format(object);
            }
        };
    }


    @Bean
    public Formatter<LocalDateTime> localDateTimeFormatter() {
        return new Formatter<>() {
            @Override
            public LocalDateTime parse(String text, Locale locale) {
                return LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME);
            }

            @Override
            public String print(LocalDateTime object, Locale locale) {
                return DateTimeFormatter.ISO_DATE_TIME.format(object);
            }
        };
    }

Bullpen answered 24/9, 2019 at 2:46 Comment(1)
Here are some guidelines for How do I write a good answer?. This provided answer may be correct, but it could benefit from an explanation. Code only answers are not considered "good" answers.Stonyhearted
O
1

Here is another general solution with parameter converter:

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import ru.diasoft.micro.msamiddleoffice.ftcaa.customerprofile.config.JacksonConfig;

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class LocalDateTimeConverter implements Converter<String, LocalDateTime>{

    private static final List<String> SUPPORTED_FORMATS = Arrays.asList("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "[another date time format ...]");
    private static final List<DateTimeFormatter> DATE_TIME_FORMATTERS = SUPPORTED_FORMATS
            .stream()
            .map(DateTimeFormatter::ofPattern)
            .collect(Collectors.toList());

    @Override
    public LocalDateTime convert(String s) {

        for (DateTimeFormatter dateTimeFormatter : DATE_TIME_FORMATTERS) {
            try {
                return LocalDateTime.parse(s, dateTimeFormatter);
            } catch (DateTimeParseException ex) {
                // deliberate empty block so that all parsers run
            }
        }

        throw new DateTimeException(String.format("unable to parse (%s) supported formats are %s",
                s, String.join(", ", SUPPORTED_FORMATS)));
    }
}
Orthorhombic answered 29/4, 2021 at 9:55 Comment(0)
J
1

This behavior occured to me whilst working with an Swagger OpenAPI Endpoint ...

The OpenAPI definition looked alike:

paths:
  /endpoint:
    get:
      summary: short summary
      operationId: endpointFunction
      parameters:
        - name: timestamp
          in: query
          description: 'Given timestamp'
          required: false
          schema:
            type: string
            format: date-time
            example: "2023-01-05T13:11:40.020747+01:00"
      responses:
        200:
          description: 'Ok'
        404:
          description: 'Not Ok'
          content: {}
        500:
          description: 'Failure'
          content: { }

After compiling the definition using Maven, it looks like this:

public interface EndpointApiDelegate {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * GET /endpoint : short summary
     *
     * @param timestamp Given timestamp (optional)
     * @return Ok (status code 200)
     *         or Not ok (status code 404)
     *         or Interner Fehler (status code 500)
     * @see EndpointApi#endpointFunction
     */
    default ResponseEntity<Void> endpointFunction(OffsetDateTime timestamp) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

Now, trying to send data directly via ..

  • a browser's URL
  • a http-file (.http)
  • postman and others
### Sample FAILING call in .http-file
GET http://localhost:{{port}}/endpoint?timestamp=22023-01-05T13:11:40.020747+01:00

.. using the given example (2023-01-05T13:11:40.020747+01:00), it fails.

"Failed to convert value of type [java.lang.String] to required type [java.time.LocalDateTime]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.time.LocalDateTime] for value ... .. .

Reasoning

Query parameters are parsed with a different charset (not sure these are the right words, but it fits the behaviour) Therefore the parsing fails reading the '+'-character.

Resolution

### Sample WORKING call in .http-file
GET http://localhost:{{port}}/endpoint?timestamp=2023-01-03T11%3A29%3A47.526612%2B01%3A00

Attention

In case the solution hopefully feels impractical ... .. .

. .. ... if it does: YOU ARE RIGHT !!!

Passing Date-Time (in this case) is a 'bad endpoint design'-decision

Swagger OpenApi offers checks for int (minimum and maximum) and string (regex-pattern) directly in the OpenApi definition.

Still Date-Time offers special pitfalls as min/max values may be dependent on relative 'everyday passing time'. I imagine automated min/max values are therefore not yet implemented.

Is there a better solution? Sure! :)

Use a POST request instead and define APPLICATION JSON in the RequestBody instead of an URL-RequestParameter.

components:
  schemas:
    TimestampEntity:
      type: object
      properties:
        timestamp:
          description: 'timestamp'
          type: string
          format: date-time
          example: "2023-01-05T13:11:40.020747+01:00"
paths:
  /endpoint:
    post:
      summary: short summary
      operationId: endpointFunction
      # parameters:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TimestampEntity'
      responses:
        200:
          description: 'Ok'
        404:
          description: 'Not Ok'
          content: {}
        500:
          description: 'Failure'
          content: { }

That compiles to:

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /endpoint : short summary
     *
     * @param timestampEntity
 (optional)
     * @return Ok (status code 200)
     *         or Not Ok (status code 404)
     *         or Failure (status code 500)
     * @see EndpointApi#endpointFunction
     */
    default ResponseEntity<Void> endpointFunction(TimestampEntity timestampEntity) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

Using a generated Timestamp.class:


public class TimestampEntity   {
  @JsonProperty("timestamp")
  private OffsetDateTime timestamp;

  public TimestampEntity timestamp(OffsetDateTime timestamp) {
    this.timestamp = timestamp;
    return this;
  }

... .. .

Now the value (2023-01-05T13:11:40.020747+01:00) will be parsed appropriately.

This is the end of being entangled in an URL charset-scuffle ;-)

Jeannettajeannette answered 13/1, 2023 at 20:24 Comment(0)
M
1

I saw the same error it got fixed as the date represented in spring was yyyy/mm/dd and i was passing yyyy-mm-dd '/' was required i was sending '-' so made the changed and it worked

Mountaintop answered 16/4, 2023 at 14:40 Comment(0)
V
1
import {DatePipe} from '@angular/common';

then in your angular constructor :

private datePipe: DatePipe

From your frontend(Angular) convert the selected date like this :

      const date = this.datePipe.transform(this.searchDashboardEntryForm.get('acceptedOn').value, 'yyyy-MM-dd');

Using date pipe now its turned to a clean date format like this : yyyy-MM-dd

From yout backend(java) make the request param like this :

            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date
Vinificator answered 17/9, 2023 at 8:14 Comment(0)
M
0

For global configuration :

public class LocalDateTimePropertyEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        setValue(LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    }

}

And then

@ControllerAdvice
public class InitBinderHandler {

    @InitBinder
    public void initBinder(WebDataBinder binder) { 
        binder.registerCustomEditor(OffsetDateTime.class, new OffsetDateTimePropertyEditor());
    }

}
Marilynnmarimba answered 4/12, 2019 at 3:12 Comment(1)
Is the LocalDateTimePropertyEditor supposed to be OffsetDateTimePropertyEditor, or vice versa?Gertiegertrud
S
0

I had a similar problem in a related context

I am using WebRequestDataBinder to map the request params to a model dynamically.

Object domainObject = ModelManager.getEntity(entityName).newInstance();
WebRequestDataBinder binder = new WebRequestDataBinder(domainObject);
binder.bind(request);

This piece of code is working for primitives but did not work for LocalDateTime type attributes

To fix the problem, before calling binder.bind, I registered a custom editor before calling bind()

binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport()
                {
                    @Override
                    public void setAsText(String text) throws IllegalArgumentException
                    {
                        setValue(LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME));
                    }

                    @Override
                    public String getAsText() {
                        return DateTimeFormatter.ISO_DATE_TIME.format((LocalDateTime) getValue());
                    }

                }
            );

This solved the problem.

Saccharo answered 29/1, 2022 at 20:7 Comment(0)
G
0

In my case

`@Configuration

//@EnableWebMvc - if uncomment this then LocalDateTime will be as arrays

public class WebConfig implements WebMvcConfigurer { ... }`

Gem answered 13/1, 2023 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.