FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
401 stars 117 forks source link

InvalidFormatException when parsing a non lenient LocalDate with german format #330

Open cradloff opened 4 days ago

cradloff commented 4 days ago

Search before asking

Describe the bug

When parsing a LocalDate with LocalDateDeserializer a InvalidFormatException occurs. The field has a pattern for german dates and is markes as not lenient. When leniency is turned on, the value gets parsed. When the pattern is removed, the value gets also parsed.

Version Information

2.18.1

Reproduction

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.OptBoolean;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import org.junit.jupiter.api.Test;

public class DateTimeParseExceptionTest {
    static class MyBean {
        @JsonSerialize(using = LocalDateSerializer.class)
        @JsonDeserialize(using = LocalDateDeserializer.class)
        @JsonFormat(pattern = "dd.MM.yyyy", lenient = OptBoolean.FALSE)
        private LocalDate geburtsdatum;

        public void setGeburtsdatum(LocalDate geburtsdatum) {
            this.geburtsdatum = geburtsdatum;
        }

        public LocalDate getGeburtsdatum() {
            return geburtsdatum;
        }
    }

    @Test
    public void dateTimeParseException() throws JsonProcessingException {
        String json = """
                { "geburtsdatum": "01.02.2000" }
                """;
        ObjectMapper mapper = new ObjectMapper();

        MyBean bean = mapper.readValue(json, MyBean.class);
        assertEquals(LocalDate.of(2000, 2, 1), bean.getGeburtsdatum());
    }
}

Expected behavior

No response

Additional context

The following exception is thrown:

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type java.time.LocalDate from String "01.02.2000": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '01.02.2000' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=2, DayOfMonth=1, YearOfEra=2000},ISO of type java.time.format.Parsed at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 19] (through reference chain: DateTimeParseExceptionTest$MyBean["geburtsdatum"]) at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67) at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1959) at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1245) at com.fasterxml.jackson.datatype.jsr310.deser.JSR310DeserializerBase._handleDateTimeException(JSR310DeserializerBase.java:176) at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer._fromString(LocalDateDeserializer.java:178) at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:91) at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer.deserialize(LocalDateDeserializer.java:37) at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:310) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177) at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4917) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828) at DateTimeParseExceptionTest.dateTimeParseException(DateTimeParseExceptionTest.java:38) at java.base/java.lang.reflect.Method.invoke(Method.java:569) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) Caused by: java.time.format.DateTimeParseException: Text '01.02.2000' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=2, DayOfMonth=1, YearOfEra=2000},ISO of type java.time.format.Parsed at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2023) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1958) at java.base/java.time.LocalDate.parse(LocalDate.java:430) at com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer._fromString(LocalDateDeserializer.java:176) ... 13 more Caused by: java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=2, DayOfMonth=1, YearOfEra=2000},ISO of type java.time.format.Parsed at java.base/java.time.LocalDate.from(LocalDate.java:398) at java.base/java.time.format.Parsed.query(Parsed.java:241) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1954) ... 15 more

cowtowncoder commented 4 days ago

Java 8 date/time handled via separate module -- will transfer to correct repo.

JooHyukKim commented 4 days ago

Happens in latest 2.17.x version as well, so might be intended behavior. May I ask what makes current behavior unexpected/incorrect, @cradloff?

JooHyukKim commented 4 days ago

So internally what happens is that when @JsonFormat is configured OptBoolean.FALSE, the formatter in LocalDate.parse(text, formater) is configured with java.time.format.ResolverStyle.STRICT, that's why it's failing.

JooHyukKim commented 4 days ago

Solution (from StackOverflow answer)

Use dd.MM.uuuu instead of yyyy when lenient = OptBoolean.FALSE.

PS : It seems like current LocalDate + JsonFormat deserialization implementation follows Java API, so maybe we could improve JavaDoc? WDYT?

cowtowncoder commented 3 days ago

Hmmh. That is very interesting @JooHyukKim. Did not realize "yyyy" won't work as well as "uuuu" in Strict mode. Apparently https://stackoverflow.com/questions/29014225/what-is-the-difference-between-year-and-year-of-era explains it but I am still not 100% sure what is missing (AD/BC indicator?)

I agree that it's not obvious what we could do here. I think lenient is even enabled by default.

JooHyukKim commented 2 days ago

You are right on point @cowtowncoder, to make yyyy word, we need to specify era and implementation would look like...

cowtowncoder commented 2 days ago

Interesting. Something new I learned then. So pattern in itself could never work in strict mode, given there is no place to give era marker.

JooHyukKim commented 2 days ago

Yeahhhh I didn't expect it either.

I'm wondering if we could improve JavaDoc somehow. To let users know that [ yyyy-pattern + lenient=FALSE ] combo wouldn't work (or might be overkill)

cowtowncoder commented 2 days ago

Could be some sort of "known gotchas" section or something, but that'd be on README.md or Wiki. Could mention on Javadocs, but this affects multiple types so probably cannot be on specific classes Javadocs.