Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
cradloff opened this issue Nov 27, 2024 · 18 comments
Open
1 task done

Comments

@cradloff
Copy link

Search before asking

  • 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

@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 28, 2024

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

@cowtowncoder cowtowncoder transferred this issue from FasterXML/jackson-databind Nov 28, 2024
@JooHyukKim
Copy link
Member

JooHyukKim commented Nov 28, 2024

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
Copy link
Member

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
Copy link
Member

JooHyukKim commented Nov 28, 2024

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
Copy link
Member

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
Copy link
Member

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\" }"

@cowtowncoder
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

@cradloff
Copy link
Author

cradloff commented Dec 2, 2024

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
Copy link
Author

cradloff commented Dec 2, 2024

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.'

@JooHyukKim
Copy link
Member

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

@cradloff
Copy link
Author

cradloff commented Dec 2, 2024

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.

@JooHyukKim
Copy link
Member

JooHyukKim commented Dec 2, 2024

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.

@cradloff
Copy link
Author

cradloff commented Dec 2, 2024

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.

@JooHyukKim
Copy link
Member

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
Copy link
Member

Maybe write like below (just my local version)

image

@cowtowncoder
Copy link
Member

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 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants