Search code examples
spring-bootjacksonspring-restcontrollerlocaldate

How to manually use springs default object mapper / manually deserializing of Http-Request Body to Object


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);
        }
    }

Solution

  • 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 LocalDateTimeto 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.