jackson-modules-java8 icon indicating copy to clipboard operation
jackson-modules-java8 copied to clipboard

InvalidFormatException when parsing a non lenient LocalDate with german format

Open cradloff opened this issue 1 year ago • 18 comments

Search before asking

  • [X] I searched in the issues and found nothing similar.

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

cradloff avatar Nov 27 '24 15:11 cradloff

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

cowtowncoder avatar Nov 28 '24 00:11 cowtowncoder

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 avatar Nov 28 '24 09:11 JooHyukKim

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 avatar Nov 28 '24 09:11 JooHyukKim

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?

JooHyukKim avatar Nov 28 '24 09:11 JooHyukKim

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.

cowtowncoder avatar Nov 28 '24 20:11 cowtowncoder

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

  • the pattern as dd.MM.yyyy G
  • and input value like "{ \"geburtsdatum\": \"01.02.2000 AD\" }"

JooHyukKim avatar Nov 30 '24 04:11 JooHyukKim

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.

cowtowncoder avatar Nov 30 '24 04:11 cowtowncoder

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)

JooHyukKim avatar Nov 30 '24 04:11 JooHyukKim

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.

cowtowncoder avatar Nov 30 '24 04:11 cowtowncoder

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

The code should not throw an exception but simply parse the value.

cradloff avatar Dec 02 '24 12:12 cradloff

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?

The workaround actually works. But I think that this is not a good workaround, as the letter u stands for the day of the week according to the documentation of SimpleDateFormat: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/SimpleDateFormat.html

Edit: I found out that the format from DateTimeFormatter are used instead of SimpleDateFormat. So please change the JavaDoc of @JsonFormat, currently it states 'pattern may contain java.text.SimpleDateFormat-compatible pattern definition.'

cradloff avatar Dec 02 '24 13:12 cradloff

Hmmm strange. Is 'u' just a work around? From what I read in the SO solution it was "year of era"?

JooHyukKim avatar Dec 02 '24 13:12 JooHyukKim

Hmmm strange. Is 'u' just a work around? From what I read in the SO solution it was "year of era"?

Sorry, I looked in the wrong place (SimpleDateFormat). See comment above.

cradloff avatar Dec 02 '24 14:12 cradloff

currently it states 'pattern may contain java.text.SimpleDateFormat-compatible pattern definition.'

@cradloff Sorry for being MIA earlier! Got caught up with house chores 🥲.

Ah, this is what you meant. But the documentation says java.util.Date to be specific. java.util.Date and java.time.LocalDate do not have any connection in terms of class hierarchy.

image

Unfortunately @JsonFormat does not actually mention anything specific about java.time.LocalDate 🥲. For @JsonFormat being too general to contain all extensions' behavior, WDYT about we add some more documentation on LocalDateDeserializer? Or we can brainstorm ways that are general enough to change @JsonFormat doc.

JooHyukKim avatar Dec 02 '24 14:12 JooHyukKim

In my opinion, JsonFormat would be the right place, because this is the place most developers look at. The documentation could point to an external location if there is not enough room in JsonFormat itself.

cradloff avatar Dec 02 '24 15:12 cradloff

Also, additional findings! Most deserializers under com.fasterxml.jackson.datatype.jsr310.deser including LocalDateDeserializer seem to use DateTimeFormatter type as formatter. I guess due to the types being all under java.time.*. So we may leverage this fact (if true) to put something up on @JsonFormat.

Great feedbacks @cradloff 👍🏼. Some word from @cowtowncoder would be great as well.

JooHyukKim avatar Dec 02 '24 15:12 JooHyukKim

Maybe write like below (just my local version)

image

JooHyukKim avatar Dec 02 '24 15:12 JooHyukKim

This does get tricky, as jackson-annotations simply provide for general "Format String" to be accessible by value serializers and deserializers, without dictating (or having ability to dictate) actual use. It is then various modules (and for java.util.Date / java.util.Calendar, main jackson-databind) that contain actual functionality to use the Format String to create actual concrete formatters.

But from practical point of view, yes, JavaDocs of JsonFormat should indicate high-level actual usage as far as we know it. So +1 for PR for improving that as suggested by @JooHyukKim .

cowtowncoder avatar Dec 03 '24 00:12 cowtowncoder

Further improvements to Javadocs: https://github.com/FasterXML/jackson-annotations/commit/54cc2e40b317dfcf8984da22c9cae36cc897b36e -- closing as completed.

cowtowncoder avatar Nov 18 '25 01:11 cowtowncoder