javalistclassjacksontype-erasure

How to deserialize generic List<T> with Jackson?


I've been using Jackson to serialize/deserialize objects for years and have always found it needlessly complicated to use TypeReference<T> to deserialize List etc. I created a simple helper function:

public static <T> TypeReference<List<T>> list() {
    return new TypeReference<List<T>>(){}
}

With intended use:

List<Foo> foos = objectMapper.readValue(json, list());

And it works! Kind of. When inspecting through the debugger, rather than a list of Foo, it is rather a list of LinkedHashMap. I understand that ObjectMapper deserializes into LinkedHashMap for type Object and I read the explanation for that here:

Jackson and generic type reference

However, why is it able to assign List<LinkedHasMap> to a List<Foo>? At the very least shouldn't that be some sort of ClassCastException?

Also, is there anyway to do this with Java's type system?

NOTE: the following method declaration has the same issue, which makes sense because the additional argument is not needed for T to be determined:

public static <T> TypeReference<List<T>> listOf(Class<T> ignored) {
    return new TypeReference<List<T>>(){}
}

Solution

  • It works like this because of type erasure in Java. Please, read about it before you start reading next part of this answer:

    As you probably know right now, after reading above articles, your method after compilation looks like this:

    static <T> TypeReference<List> listOf(Class<T> ignored) {
        return new TypeReference<List>(){};
    }
    

    Jackson will try to find out the most appropriate type for it which will be java.util.LinkedHashMap for a JSON object. To create an irrefutable type, you need to use com.fasterxml.jackson.databind.type.TypeFactory class. See below example:

    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.JavaType;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    
    import java.io.File;
    import java.util.List;
    
    public class JsonTypeApp {
    
        public static void main(String[] args) throws Exception {
            File jsonFile = new File("./resource/test.json").getAbsoluteFile();
    
            ObjectMapper mapper = new ObjectMapper();
    
            System.out.println("Try with 'TypeFactory'");
            List<Id> ids = mapper.readValue(jsonFile, CollectionsTypeFactory.listOf(Id.class));
            System.out.println(ids);
            Id id1 = ids.get(0);
            System.out.println(id1);
    
            System.out.println("Try with 'TypeReference<List<T>>'");
            List<Id> maps = mapper.readValue(jsonFile, CollectionsTypeFactory.erasedListOf(Id.class));
            System.out.println(maps);
            Id maps1 = maps.get(0);
            System.out.println(maps1);
        }
    }
    
    class CollectionsTypeFactory {
        static JavaType listOf(Class clazz) {
            return TypeFactory.defaultInstance().constructCollectionType(List.class, clazz);
        }
    
        static <T> TypeReference<List> erasedListOf(Class<T> ignored) {
            return new TypeReference<List>(){};
        }
    }
    
    class Id {
        private int id;
    
        // getters, setters, toString
    }
    

    Above example, for below JSON payload:

    [
      {
        "id": 1
      },
      {
        "id": 22
      },
      {
        "id": 333
      }
    ]
    

    prints:

    Try with 'TypeFactory'
    [{1}, {22}, {333}]
    {1}
    Try with 'TypeReference<List<T>>'
    [{id=1}, {id=22}, {id=333}]
    Exception in thread "main" java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.example.Id
        at com.example.JsonTypeApp.main(JsonTypeApp.java:27)
    

    See also: