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”
Advertisement
Answer
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("{rn" + " "birthDate": "1999-01-01"rn" + "}", 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("{rn" + " "birthDate": 1rn" + "}", DateContainer.class); System.out.println(container.getBirthDate()); }); }
EDIT
Deserializer uses Pattern