I have an generic Rest-Constroller which needs to construct an entity out of an entity-name an request body representing the entities properties. I need to manually map the request body to an target class.
I already tried different ways to customize the way my RestController deserializes the data and think my current approach is the easiest. I now register an @PostMapping and retrieve the entityName which i use to determine the correct class and an @RequestBody String entityJson which i convert via an ObjectMapper to the determined class.
yet i encounter problems during LocalDate deserialiing (Note: my date strings are of format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ' where times are zeros).
I use springboot 2.1.6.RELEASE with jpa 2.1.9 and therefore do not need custom handling of LocalDate/LocalDateTime properties in my RestControllers if i use an @RequestBody XXXEntity entity parameter. But when injecting an OnjectMapper bean to my RestController and try to objectMapper.readValue(entityJson, entityClass) deserialisation fails on LocalDate properties.
How can i deserialize the way spring does (correctly) if the entity class is known upfront?
1.) Exception without custom configuration:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of java.time.LocalDate
(no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-10-29T00:00:00.000Z')
2.) When using an custom JSR310Module ObjecktMapper
java.time.format.DateTimeParseException: Text '2019-10-29T00:00:00.000Z' could not be parsed, unparsed text found at index 10
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.modules(new JSR310Module())
.build();
Same error with custom DateFormat:
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
objectMapper.setDateFormat(df);
I tried registering the LocalDateTime Deserializer for LocalDate das well:
Jackson2ObjectMapperBuilder.json()
.deserializerByType(LocalDate.class, LocalDateTimeDeserializer.INSTANCE)
Result gets better... so paring fails where i hit the UTC timezon: java.time.format.DateTimeParseException: Text '2019-10-29T00:00:00.000Z' could not be parsed, unparsed text found at index 23
This is how my Controller looks like:
@Autowired
public AdminRestController(EntityService entityService, ObjectMapper objectMapper) {
this.entityService = entityService;
this.objectMapper = objectMapper;
}
...
@Transactional
@PostMapping(value = "/entities/{entityName}")
public List<T> createEntity(@PathVariable String entityName, @RequestBody String entityJson) {
T newEntity = constructEntity(entityName, entityJson);
entityService.create(newEntity);
...
}
...
private T constructEntity(String entityName, String entityJson) {
Class<T> entityClass;
try {
entityClass = findClassByName(entityName);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Entity " + entityName + " unknwon.");
}
try {
T entity = objectMapper.readValue(entityJson, entityClass);
return entity;
} catch (IOException e) {
throw new IllegalStateException("Unable to construct entity " + entityName + " from provided data.", e);
}
}
You may consider it just as hacky. My version of the logic would be:
if (string.isEmpty()) {
return null;
}
if (string.endsWith("Z")) {
return Instant.parse(string).atZone(ZONE_SWISS).toLocalDate();
}
return LocalDate.parse(string, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
I have avoided the modification of the string and the conversion from LocalDateTime
to LocalDate
. Also since the Z
can only come last in the string, my version gives preciser validation. Finally as a matter of taste I like the String.isEmpty
method.