Search code examples
javagenericsjacksondeserialization

Jackson Deserialization of Objects Extending Abstract Classes Using Generics


I've seen plenty of examples of how to deal with Jackson deserialization when using abstract classes and generics, but I'm trying to understand why type inference isn't possible in certain cases. Consider the following:

Objects:

public abstract class BaseObject {
    public String someField;
}

public class ConcreteObjectOne extends BaseObject {}

public class ConcreteObjectTwo extends BaseObject {}

Classes deserializing objects:

public abstract class BaseService<T extends BaseObject> {
    public T deserializeObjectFail(String json) throws JsonProcessingException {
        var mapper = JsonMapper.builder().build();

        return mapper.readValue(json, new TypeReference<T>() {});
    }

    public T deserializeObjectSuccess(String json, TypeReference<T> typeReference) throws JsonProcessingException {
        var mapper = JsonMapper.builder().build();

        return mapper.readValue(json, typeReference);
    }
}

public class ConcreteServiceOne extends BaseService<ConcreteObjectOne> {}

public class ConcreteServiceTwo extends BaseService<ConcreteObjectTwo> {}

Tests:

var serviceOne = new ConcreteServiceOne();
var serviceTwo = new ConcreteServiceTwo();

// These fail with an InvalidDefinitionException
serviceOne.deserializeObject("{\"someField\":\"one\"}");
serviceTwo.deserializeObject("{\"someField\":\"two\"}");

// These succeed
serviceOne.deserializeObjectFixed("{\"someField\":\"one\"}", new TypeReference<>() {});
serviceTwo.deserializeObjectFixed("{\"someField\":\"two\"}", new TypeReference<>() {});

Why does the first example fail while the second succeeds? It seems like the generic type should be known in both cases. The class definitions of ConcreteServiceOne and ConcreteServiceTwo provide the concrete object types that T represents.

So shouldn't new TypeReference<T>() {} be able to make use of the types provided for T by the concrete classes? Why would that fail, while using TypeReference<T> as a parameter provided by the concrete classes work even when the concrete classes are using type inference by passing in new TypeReference<>() {}? It seems like the type of T should be able to be inferred equally in both cases, especially considering the generic type can even be omitted in the second example.


Solution

  • In generics, the compiler ensures your type are safe at compilation phase. After that the compiler removes all type information (keyword: type erasure) and convert the objects into type Object for runtime. Thus, your type is basically Object in runtime (without Jackson's explicit TypeReference, deserialization would fail). use. Jackson API lib says that in order to retain type information at runtime, they have TypeReference.

    The topic of generics is quite large, but if you want to look for into type erasure, here's the document from Oracle: Type Erasure

    I hope I answered your question.