Search code examples
javajacksonlocaldatetime

Jackson deserialization of LocalDateTime with custom format in Java 11


I think I've read every past post on this issue and it still isn't working for me. My setup:

OpenJDK 64-Bit 11.0.14.1+1 Jackson module version 2.14.1

I have the following Jackson modules included:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-parameter-names</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jdk8</artifactId>
    </dependency>

My mapper is instantiated as such having all the relevant Java dependencies above:

ObjectMapper jsonObjectMapper = new ObjectMapper()
        .registerModule(new ParameterNamesModule())
        .registerModule(new Jdk8Module())
        .registerModule(new JavaTimeModule());

And my Java objects have various LocalDateTime properties defined as follows with the relevant annotations:

@JsonProperty("created")
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
@JsonDeserialize(using= LocalDateTimeDeserializer.class) //Tried with and without this line
private LocalDateTime created;

But when it parses I get the error:

Cannot deserialize value of type `java.time.LocalDateTime` from String "2021-10-15": 
Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) 
Text '2021-10-15' could not be parsed: Unable to obtain LocalDateTime from 
TemporalAccessor: {},ISO resolved to 2021-10-15 of type java.time.format.Parsed

So it seems to not be using the custom formatter. I then tried adding DateTimeFormatter directly using the following:

JavaTimeModule jtm = new JavaTimeModule();
jtm.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
ObjectMapper mapper = new ObjectMapper()
  .registerModule(new ParameterNamesModule())
  .registerModule(new Jdk8Module())
  .registerModule(jtm);

But that didn't help. What am I missing?


Solution

  • Since java.time API fails to construct LocalDateTime objects from strings with missing time part (there are obvious defaults though), you have following options:

    1. take advantage of getters and setters in order to perform LocalDate -> LocalDateTime conversion and vice versa:
    @JsonIgnore
    private LocalDateTime created;
    
    @JsonProperty("created")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    public LocalDate getCreatedJson() {
        return Optional.ofNullable(created)
                .map(LocalDateTime::toLocalDate)
                .orElse(null);
    }
    
    @JsonProperty("created")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    public void setCreatedJson(LocalDate localDate) {
        created = Optional.ofNullable(localDate)
                .map(LocalDate::atStartOfDay)
                .orElse(null);
    }
    
    1. use @JsonDeserialize#converter feature of jackson:

    Which helper object (if any) is to be used to convert from Jackson-bound intermediate type (source type of converter) into actual property type (which must be same as result type of converter). This is often used for two-step deserialization; Jackson binds data into suitable intermediate type (like Tree representation), and converter then builds actual property type.

    public class LocalDateTimeConverter extends StdConverter <LocalDate, LocalDateTime> {
    
        @Override
        public LocalDateTime convert(LocalDate localDate) {
            return localDate.atStartOfDay();
        }
    }
    
    ...
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    @JsonDeserialize(converter = LocalDateTimeConverter.class)
    private LocalDateTime created;
    
    
    1. create your own deserializer, for example:
    public class EnhancedLocalDateTimeDeserializer extends LocalDateTimeDeserializer {
            
        protected EnhancedLocalDateTimeDeserializer() {
            this(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        }
    
        public EnhancedLocalDateTimeDeserializer(DateTimeFormatter formatter) {
            super(new DateTimeFormatterBuilder()
                    .append(formatter)
                    .parseDefaulting(HOUR_OF_DAY, 0)
                    .parseDefaulting(MINUTE_OF_HOUR, 0)
                    .parseDefaulting(SECOND_OF_MINUTE, 0)
                    .parseDefaulting(NANO_OF_SECOND, 0)
                    .toFormatter());
        }
    
        protected EnhancedLocalDateTimeDeserializer(EnhancedLocalDateTimeDeserializer base, Boolean leniency) {
            super(base, leniency);
        }
    
        @Override
        protected EnhancedLocalDateTimeDeserializer withDateFormat(DateTimeFormatter formatter) {
            return new EnhancedLocalDateTimeDeserializer(formatter);
        }
    
        @Override
        protected EnhancedLocalDateTimeDeserializer withLeniency(Boolean leniency) {
            return new EnhancedLocalDateTimeDeserializer(this, leniency);
        }
    
        @Override
        protected EnhancedLocalDateTimeDeserializer withShape(JsonFormat.Shape shape) {
            return this;
        }
    
    }
    
    1. use LocalDate instead of LocalDateTime