Search code examples
javaspringjacksondeserializationmixins

How to deserialize Spring's ResponseEntity with Jackson ObjectMapper (probably with using @JsonCreator and Jackson mixins)


The class ResponseEntity doesn't have a default constructor. Then in order to deserialize it with objectMapper I decided to use the approach given by araqnid in that answer. Shortly - it needs to use mixin feature of Jackson coupled with @JsonCreator.

In my case (with ResponseEntity) it doesn't work out yet due to different reasons.

My test method looks like this:

public static void main(String[] args) throws Exception {
    ResponseEntity<Object> build = ResponseEntity.ok().build();

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.addMixIn(ResponseEntity.class, ResponseEntityMixin.class);

    String s = objectMapper.writeValueAsString(build);
    ResponseEntity<Object> result = objectMapper.readValue(s, ResponseEntity.class);

    System.out.println(result);
}

Firstly, I tried to use the shortest constructor for mixin:

public abstract static class ResponseEntityMixin {
    @JsonCreator
    public ResponseEntityMixin(@JsonProperty("status") HttpStatus status) {
    }
}

In this case I got an assertion error since ResponseEntity has this line of code inside it's constructor:

Assert.notNull(status, "HttpStatus must not be null");

Then I switched the mode of @JsonCreator to DELEGATING but in this case I got another exception:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `org.springframework.http.HttpStatus` from String "headers": value not one of declared Enum instance names: [UPGRADE_REQUIRED, UNAVAILABLE_FOR_LEGAL_REASONS, SERVICE_UNAVAILABLE, CHECKPOINT, LOCKED, METHOD_FAILURE, FAILED_DEPENDENCY, UNPROCESSABLE_ENTITY, PROCESSING, PROXY_AUTHENTICATION_REQUIRED, METHOD_NOT_ALLOWED, GONE, MULTIPLE_CHOICES, GATEWAY_TIMEOUT, ACCEPTED, TEMPORARY_REDIRECT, INTERNAL_SERVER_ERROR, URI_TOO_LONG, LOOP_DETECTED, PAYLOAD_TOO_LARGE, EXPECTATION_FAILED, MOVED_TEMPORARILY, REQUEST_ENTITY_TOO_LARGE, NOT_EXTENDED, CREATED, RESET_CONTENT, BAD_GATEWAY, CONFLICT, VARIANT_ALSO_NEGOTIATES, NETWORK_AUTHENTICATION_REQUIRED, NOT_FOUND, LENGTH_REQUIRED, INSUFFICIENT_SPACE_ON_RESOURCE, NO_CONTENT, OK, FOUND, SEE_OTHER, BANDWIDTH_LIMIT_EXCEEDED, REQUEST_HEADER_FIELDS_TOO_LARGE, PERMANENT_REDIRECT, NOT_ACCEPTABLE, MOVED_PERMANENTLY, REQUEST_TIMEOUT, UNAUTHORIZED, USE_PROXY, IM_USED, ALREADY_REPORTED, PARTIAL_CONTENT, PRECONDITION_FAILED, REQUEST_URI_TOO_LONG, BAD_REQUEST, INSUFFICIENT_STORAGE, CONTINUE, NON_AUTHORITATIVE_INFORMATION, REQUESTED_RANGE_NOT_SATISFIABLE, UNSUPPORTED_MEDIA_TYPE, I_AM_A_TEAPOT, HTTP_VERSION_NOT_SUPPORTED, SWITCHING_PROTOCOLS, NOT_MODIFIED, NOT_IMPLEMENTED, TOO_MANY_REQUESTS, DESTINATION_LOCKED, PAYMENT_REQUIRED, FORBIDDEN, PRECONDITION_REQUIRED, MULTI_STATUS]
 at [Source: (String)"{"headers":{},"body":null,"statusCode":"OK","statusCodeValue":200}"; line: 1, column: 2]

Also I tried to use the all-arguments constructor of ResponseEntity but it was also unsuccessful because of problems with MultiValueMap deserialization (but I believe that if I fixed it then it would finally bring me to the same problem as described above).

Could anyone help me solving this problem? Maybe it's impossible to use mixins in this case at all?

If you know the another approaches how to deserialize ResponseEntity with Jackson - please give them too.

Here is the source code of my tests: https://github.com/amseager/responseentitydeserialization


Solution

  • Using MixIns is a good way to solve it:

    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            ResponseEntity<Object> entity = ResponseEntity
                    .ok()
                    .header("header", "value")
                    .body("Everything is OK!");
    
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.addMixIn(ResponseEntity.class, ResponseEntityMixin.class);
            objectMapper.addMixIn(HttpStatus.class, HttpStatusMixIn.class);
    
            String json = objectMapper.writeValueAsString(entity);
            TypeReference ref = new TypeReference<ResponseEntity<Object>>() {
            };
            ResponseEntity<Object> result = objectMapper.readValue(json, ref);
            System.out.println(result);
            System.out.println(result.equals(entity));
        }
    }
    
    @JsonIgnoreProperties(ignoreUnknown = true)
    class ResponseEntityMixin {
        @JsonCreator
        public ResponseEntityMixin(@JsonProperty("body") Object body,
                                   @JsonDeserialize(as = LinkedMultiValueMap.class) @JsonProperty("headers") MultiValueMap<String, String> headers,
                                   @JsonProperty("statusCodeValue") HttpStatus status) {
        }
    }
    
    class HttpStatusMixIn {
    
        @JsonCreator
        public static HttpStatus resolve(int statusCode) {
            return HttpStatus.NO_CONTENT;
        }
    }
    

    Above code prints:

    <200 OK OK,Everything is OK!,[header:"value"]>
    

    and
    true which means source and deserialised objects are the same.