jackson-modules-java8
jackson-modules-java8 copied to clipboard
InvalidFormatException when parsing a non lenient LocalDate with german format
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
Java 8 date/time handled via separate module -- will transfer to correct repo.
Happens in latest 2.17.x version as well, so might be intended behavior. May I ask what makes current behavior unexpected/incorrect, @cradloff?
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.
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?
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.
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\" }"
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.
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)
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.
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.
Solution (from StackOverflow answer)
Use
dd.MM.uuuuinstead ofyyyywhenlenient = 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.'
Hmmm strange. Is 'u' just a work around? From what I read in the SO solution it was "year of era"?
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.
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.
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.
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.
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.
Maybe write like below (just my local version)
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 .
Further improvements to Javadocs: https://github.com/FasterXML/jackson-annotations/commit/54cc2e40b317dfcf8984da22c9cae36cc897b36e -- closing as completed.