4

Example code below.

Using Spring Boot 2.2, I want to communicatie with a REST API. the API I'm trying to consume wraps objects in a parent model for paging and sorting and puts an json array of the actual objects in the results field. How would I model my Java code so jackson 'knows' how to deserialize the API responses into my java objects?

I've tried solving this using an generic in ApiResponse, and passing the expected field-type when performing the get request:

String URL_GET_DOGS = "https://localhost/api/v1/dogs/"
ApiResponse<Dog> response = this.restTemplate.getForObject(URL_GET_DOGS, response.getClass());

This compiles AND runs...

Expected result: Successfully created an ApiResponse object with a results field consisting out of a List of Dogs.

Actual result: Successfully created an ApiResponse object but the results field is a List of Objects.

So jackson won't cast the results list properly and instead I appear to get a List<Object> instead of List<Dog> for my results field in my ApiResponse Object. This way I end up with properties of the wrong Type or properties that I don't want to deserialize at all! See Car example.

Now I'm back to an interface-based solution but I'm stuck. Jackson (rightfully, because there is no way to deduce the correct class...) complains that it does not know how to deserialize abstract types and I would need to provide a concrete implementation, I can't use class-type Jackson annotations as described here because I do not control the API generating the responses.

The only way out of this I see right now, is to use classes for each type of response but that means a lot of duplicate code for paging and sorting fields. What am I doing wrong?

Example JSON:

{
    "count": 84,
    "next": "http://localhost:80/api/v1/dogs/?limit=2&offset=2",
    "previous": null,
    "results": [
        {
            "name": "Pebbles"
        },
        {
            "name": "Spot"
        }
    ]
}

and another endpoint:

{
    "count": 22,
    "next": "http://localhost:80/api/v1/cars/?limit=2&offset=2",
    "previous": null,
    "results": [
        {
            "brand": "Mercedes",
            "horse_power": 120,
            "field_i_dont": "want_to_deserialize"
        },
        {
            "brand": "BMW",
            "horse_power": 180,
            "field_i_dont": "want_to_deserialize"
        }
    ]
}

Example code:

public class ApiResponse<T>{

    // paging and sorting
    private Long count;
    private String next;
    private String previous;
    // the actual objects
    private List<T> results;

    // No-args constructor, getters & setters

}

public class Dog {
     private String name;
    // No-args constructor, getters & setters

}

@JsonIgnoreProperties(ignoreUnknown = true)
public class Car {
     private int horsePower;
     private String brand;
    // No-args constructor, getters & setters

}
J. Roovers
  • 71
  • 1
  • 8

1 Answers1

1

Cause

Upon further investigation I found this is caused by Type Erasure, an answer here on stackoverflow provided a snippet to deal with this using Jackson.

From what I now understand the call to response.getClass() is exactly the same as calling ApiResponse.class. Even though ApiResponse was typed as a class with a generic parameter, generics 'lose' their type parameter because of a compiler rule called 'Type Erasure'. The Internal generic is converted to an Object type and Jackson will use a LinkedHashMap to represent any data in Object fields.

Generics are used for tighter type checks at compile time and to provide a generic programming. To implement generic behaviour, java compiler apply type erasure. Type erasure is a process in which compiler replaces a generic parameter with actual class or bridge method. In type erasure, compiler ensures that no extra classes are created and there is no runtime overhead.

Type Erasure rules

  • Replace type parameters in generic type with their bound if bounded type parameters are used.
  • Replace type parameters in generic type with Object if unbounded type parameters are used.
  • Insert type casts to preserve type safety.
  • Generate bridge methods to keep polymorphism in extended generic types.

Solution

In order to 'fix' the example in my original question

ApiResponse<Dog> response = this.restTemplate.getForObject(URL_GET_DOGS, response.getClass())

Had to be changed so the raw JSON could be mapped using the JavaType class:

ObjectMapper mapper = new ObjectMapper(); // Defined as final in rest-client class.
String rawJsonResponse = this.restTemplate.getForObject(URL_GET_DOGS, String.class)
ApiResponse<Dog> response = mapper.readValue(
    rawJsonResponse,
    mapper.getTypeFactory().constructParametricType(
        ApiResponse.class,
        Dog.class)
    );

Community
  • 1
  • 1
J. Roovers
  • 71
  • 1
  • 8