How do I globally enable “strict” handling of LocalDate values using Jackson?



Jackson Java 8 Date/time module issue jackson-modules-java8#212 mentions enabling “strict” handling via a global default:

Currently LocalDateDeserializer automatically accepts non-standard format where time part also exists. While this is useful for some use cases, compatibility, it seems reasonable that if user forces “strict” handling (via @JsonFormat, or global default), that part would not be accepted.

However, I am completely failing at figuring out how to enable this globally. I’ve checked configuration classes such as DeserializationFeature and done Internet & Stack Overflow searches for this, but have not come up with an answer.

How can I enable “strict” handling of LocalDate, such that @JsonFormat behaves the same as @JsonFormat(lenient = OptBoolean.FALSE) for values deserialized by Jackson?

Answer

You can set global leniency by com.fasterxml.jackson.databind.ObjectMapper#setDefaultLeniency.

But this won’t fail when LocalDate contains the time part too. Since there is strange logic in com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer. It uses DateTimeFormatter ISO_LOCAL_TIME as default formatter, which doesn’t allow time part, but when deserializer parses datetime, this code runs:

try {
    // as per [datatype-jsr310#37], only check for optional (and, incorrect...) time marker 'T'
    // if we are using default formatter
    DateTimeFormatter format = _formatter;
    if (format == DEFAULT_FORMATTER) {
        // JavaScript by default includes time in JSON serialized Dates (UTC/ISO instant format).
        if (string.length() > 10 && string.charAt(10) == 'T') {
           if (string.endsWith("Z")) {
               return LocalDateTime.ofInstant(Instant.parse(string), ZoneOffset.UTC).toLocalDate();
           } else {
               return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
           }
        }
    }
    return LocalDate.parse(string, format);
} catch (DateTimeException e) {
    return _handleDateTimeException(ctxt, e, string);
}

You can avoid this by duplicating default DateTimeFormatter. Here is my proposal for this:

class ObjectMapperPlayground {

    @Data
    static class Test {
        LocalDate date;
    }

    private static DateTimeFormatter clone(DateTimeFormatter dtf) {
        ResolverStyle oldStyle = dtf.getResolverStyle();
        ResolverStyle newStyle = Arrays.stream(ResolverStyle.values())
                .filter(Predicate.not(Predicate.isEqual(oldStyle)))
                .findAny()
                // should never happen
                .orElseThrow(() -> new IllegalStateException("ResolverStyle enum has single value."));
        // DateTimeFormatter has a check which makes our life a little harder :)
        /*
            public DateTimeFormatter withResolverStyle(ResolverStyle resolverStyle) {
                Objects.requireNonNull(resolverStyle, "resolverStyle");
                if (Objects.equals(this.resolverStyle, resolverStyle)) {
                    return this;
                }
                return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone);
            }
         */
        return dtf.withResolverStyle(newStyle).withResolverStyle(oldStyle);
    }

    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper regularMapper = new ObjectMapper()
                // no clone here
                .registerModule(new JavaTimeModule().addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)))
                .setDefaultLeniency(false);

        // OK
        regularMapper.readValue("{"date":"2021-01-01T12:12:12"}", Test.class);

        ObjectMapper dupMapper = new ObjectMapper()
                // with clone
                .registerModule(new JavaTimeModule().addDeserializer(LocalDate.class, new LocalDateDeserializer(clone(DateTimeFormatter.ISO_LOCAL_DATE))))
                .setDefaultLeniency(false);

        // throws
        dupMapper.readValue("{"date":"2021-01-01T12:12:12"}", Test.class);
    }
}

Jackson databind 2.13

As for version 2.13 of JavaTime module, lenient check was added for disallowing time part in LocalDate. So workaround with a copy of DateTimeFormatter is not needed, setDefaultLeniency(false) should be enough.



Source: stackoverflow