Search code examples
javajson-deserializationjsr310

How to restrict jackson from parsing millis to LocalDate in json request


I need to validate LocalDate fields in json requests. What i want is to prevent deserializing numbers as miilis to LocalDate. Here is example:

I have an entity:

public class Test {

   @NotNull
   @JsonFormat(pattern = "yyyy-MM-dd")
   private LocalDate birthDate;

   //getter and setter of course

}

Jackson2ObjectMapperBuilder config:

@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
    builder.featuresToEnable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
    builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    builder.modulesToInstall(new JavaTimeModule());
    return builder;
}

Now if i'm receiveing:

{
    "birthDate": 1
}

the result is birthDate=1970-01-02

I'm able to do so by setting leniency to false:

objectMapper.configOverride(LocalDate.class).setFormat(JsonFormat.Value.forLeniency(false));
objectMapper.configOverride(LocalDateTime.class).setFormat(JsonFormat.Value.forLeniency(false));

And then it's working by throwing MismatchedInputException

But it's a little brutal to backward compatibility of our service, because we need to change all our date patterns from "yyyy-MM-dd" to "uuuu-MM-dd" and i wonder is there some solution to say jackson "If you see numbers or anything different from the pattern while deserialization, throw an exception"


Solution

  • You could write a custom LocalDateDeserializer:

    public class MyLocalDateDeserializer extends JsonDeserializer<LocalDate> implements ContextualDeserializer {
    
        private LocalDateDeserializer defaultDeserializer = new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    
        public MyLocalDateDeserializer() {
            super();
        }
    
        public MyLocalDateDeserializer(LocalDateDeserializer defaultDeserializer) {
            super();
            this.defaultDeserializer = defaultDeserializer;
        }
    
    
        @Override
        public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException
        {
            if (StringUtils.isNumeric(parser.getText())) {
                throw  JsonMappingException.from(parser, "Not a String representation of Date ");
    
            }
            return defaultDeserializer.deserialize(parser, context);
        }
    
    
        @Override
        public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
                BeanProperty property) throws JsonMappingException
        {
            JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
            return (format == null) ? this : new MyLocalDateDeserializer(new LocalDateDeserializer(DateTimeFormatter.ofPattern(format.getPattern())));
        }
    
        protected JsonFormat.Value findFormatOverrides(DeserializationContext ctxt,
                BeanProperty prop, Class<?> typeForDefaults)
        {
            if (prop != null) {
                return prop.findPropertyFormat(ctxt.getConfig(), typeForDefaults);
            }
            // even without property or AnnotationIntrospector, may have type-specific defaults
            return ctxt.getDefaultPropertyFormat(typeForDefaults);
        }
    
    }
    

    and register it when needed.

    Here my simple Tests:

    @Test()
    public void testObjectMapperForLocalDate() throws IOException {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDate.class, new MyLocalDateDeserializer());
        builder.modulesToInstall(javaTimeModule);
        ObjectMapper objectMapper =  builder.build();
    
           DateContainer container =  objectMapper.readValue("{\r\n" +
                    "    \"birthDate\": \"1999-01-01\"\r\n" +
                    "}", DateContainer.class);
               System.out.println(container.getBirthDate());
    }
    
    @Test()
    public void testFailObjectMapperForLocalDate() throws IOException {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(LocalDate.class, new MyLocalDateDeserializer());
        builder.modulesToInstall(javaTimeModule);
        ObjectMapper objectMapper =  builder.build();
    
        assertThrows(JsonMappingException.class, () -> {
           DateContainer container =  objectMapper.readValue("{\r\n" +
                    "    \"birthDate\": 1\r\n" +
                    "}", DateContainer.class);
               System.out.println(container.getBirthDate());
          });
    }
    

    EDIT

    Deserializer uses Pattern