[FIXED] Spring can't parse valid LocalDateTime

Issue

I don’t understand, what is wrong here? The date seems correct to me:

java.time.format.DateTimeParseException: Text '1971-04-29T00:00:00' could not be parsed at index 2
at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1948)
at java.base/java.time.LocalDateTime.parse(LocalDateTime.java:492)
at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:75)
at org.springframework.format.datetime.standard.TemporalAccessorParser.parse(TemporalAccessorParser.java:46)
...

If I just run this code, it works, so I assume it is some kind of Spring problem:

LocalDateTime.parse("1971-04-29T00:00:00")

The corresponding spring controller:

@Controller
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class RecordController(
    val recordRepository: RecordRepository
) {

    @PostMapping("/records")
    @ResponseBody
    @ResponseStatus(value = HttpStatus.CREATED)
    fun createRecord(
        @RequestBody
        record: RecordEntity
    ): RecordEntity {
        return recordRepository.save(record)
    }

    @GetMapping("/records")
    @ResponseBody
    fun getRecords(
        @RequestParam("from", required = false)
        from: LocalDateTime?,
        @RequestParam("to", required = false)
        to: LocalDateTime?,
        @RequestParam("client-type", required = false)
        clientType: ClientType?
    ): Iterable<RecordEntity> {
        var spec: Specification<RecordEntity> = where(null)!!
        if (from != null)
            spec = spec.andFrom(from)
        if (to != null)
            spec = spec.andTo(to)
        if (clientType != null)
            spec = spec.andClientTypeSpec(clientType)
        return recordRepository.findAll(spec)
    }

    fun Specification<RecordEntity>.andFrom(from: LocalDateTime): Specification<RecordEntity> {
        return and { root: Root<RecordEntity>, criteriaQuery: CriteriaQuery<*>?, criteriaBuilder: CriteriaBuilder ->
            criteriaBuilder.greaterThan(root.get<LocalDateTime>("createdAt"), from)
        } ?: this
    }

    fun Specification<RecordEntity>.andTo(to: LocalDateTime): Specification<RecordEntity> {
        return and { root: Root<RecordEntity>, criteriaQuery: CriteriaQuery<*>?, criteriaBuilder: CriteriaBuilder ->
            criteriaBuilder.lessThan(root.get<LocalDateTime>("createdAt"), to)
        } ?: this
    }

    fun Specification<RecordEntity>.andClientTypeSpec(clientType: ClientType): Specification<RecordEntity> {
        return and { root: Root<RecordEntity>, criteriaQuery: CriteriaQuery<*>?, criteriaBuilder: CriteriaBuilder ->
            criteriaBuilder.equal(root.get<ClientType>("clientType"), clientType)
        } ?: this
    }
}

The Entity Object:

@Entity
@Table(name = "RECORDS")
class RecordEntity(
    @Id @Column(name = "BOOKING_ID")
    var bookingId: String,
    @Column(name = "CLIENT")
    @Enumerated(EnumType.STRING)
    var clientType: ClientType,
    @Column(name = "CREATED_AT", columnDefinition = "TIMESTAMP")
    var createdAt: LocalDateTime
)

The Repository is rather boring:

@Repository
interface RecordRepository : CrudRepository<RecordEntity, String>, JpaSpecificationExecutor<RecordEntity> {

}

The Rest call:

curl --location --request GET 'localhost:8080/records?to=1971-04-29T00:00:00'

I’m not sure where the problem comes from, so I included all the code. Thanks for helping 🙂

Solution

Spring has a number of different conversion utilities for JDK types, like numbers and dates.

Specifically, Spring Boot, in order to setup the Spring MVC web infrastructure, uses DateTimeFormatterRegistrar to register a bunch of different formatters for the date types in java.time.

As of this writing, for a LocalDateTime, it uses

DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)

For my system’s Locale, this DateTimeFormatter produces formats like

12/19/20, 8:45 PM

From your error,

Text '1971-04-29T00:00:00' could not be parsed at index 2

I assume it does the same for parsing: index 2 would’ve been the position of the /.

There are a number of ways to fix this, like registering your own DateTimerFormatterRegistrar. The easiest solution, in my opinion, is to simply annotate your parameter with an appropriate @DateTimeFormat. For example,

@RequestParam @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime to

This will force Spring MVC to use a separate DateTimeFormatter instance, capable of parsing your date-time string.

Answered By – Sotirios Delimanolis

Answer Checked By – Cary Denson (Easybugfix Admin)

Leave a Reply

(*) Required, Your email will not be published