Search code examples
javajacksonquarkusjackson-databind

Jackson uses InstantDeserializer instead of registered CustomDeserializer to deserialize OffsetDateTime in Quarkus


Front-end sends dates in format: 2024-06-01T05:55:04 from datetime-local control.

Example REST controller:

@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ExampleResource {
  @POST
  public RestResponse<Example> create(Example example) {
    return RestResponse.ok(exampleService.create(example));
  }
}

@Data
public class Example {
    @JsonProperty("creationTime")
    private OffsetDateTime creationTime;  // from front-end comes 2024-06-01T05:55:04
}

Custom deserializer for OffsetDateTime is added here:

@Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {

  public void customize(ObjectMapper mapper) {
    var module = new SimpleModule();
    module.addDeserializer(OffsetDateTime.class, new CustomOffsetDateTimeDeserializer());
    mapper.registerModule(module);
  }

  static class CustomOffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    // figured out that this method is not called during deserialization
    @Override
    public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
      String dateString = p.getText();
      try {
        return OffsetDateTime.parse(dateString);
      } catch (Exception e) {
        var localDateTime = LocalDateTime.parse(dateString, FORMATTER);
        return localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(localDateTime));
      }
    }

  }

}

I tried another approach - override default ObjectMapper bean:

  @Produces
  @Singleton
  public ObjectMapper objectMapper() {
    var module = new SimpleModule();
    module.addDeserializer(OffsetDateTime.class, new CustomOffsetDateTimeDeserializer());
    var objectMapper = new ObjectMapper();
    objectMapper.registerModule(module);
    return objectMapper;
  }

None of the above works and Quarkus fails to deserialize dates. Browser gets HTTP 400 Bad Request response without body.

I ran debugger and figured out that the default InstantDeserializer is used to deserialize OffsetDateTime.

How to properly configure custom deserializer for OffsetDateTime?

Should I use another approach to parse dates like 2024-06-01T05:55:04 to OffsetDateTime?


Solution

  • Finally created a mixin and it works.

    @Singleton
    public class ObjectMapperConfig implements ObjectMapperCustomizer {
    
      public void customize(ObjectMapper mapper) {
        mapper.addMixIn(OffsetDateTime.class, DateMixin.class); // add mixin
      }
    
      // This mixin will be used to deserialize OffsetDateTime
      @JsonDeserialize(converter = CustomOffsetDateTimeConverter.class)
      static class DateMixin {
      }
    
      // Define a converter from String to OffsetDateTime
      static class CustomOffsetDateTimeConverter extends StdConverter<String, OffsetDateTime> {
    
        // We will try to parse this format on error
        private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
    
        @Override
        public OffsetDateTime convert(String dateString) {
          try {
    
            // First try to parse as OffsetDateTime
            return OffsetDateTime.parse(dateString);
    
          } catch (Exception e) {
    
            // Then continue to parse using our format (without offset)
            var localDateTime = LocalDateTime.parse(dateString, FORMATTER);
    
            // Now use system default timezone to convert local datetime to offset datetime
            return localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(localDateTime));
    
          }
        }
      }
    
    }