Jackson and Generic Type Reference

Jackson and generic type reference

This is a well-known problem with Java type erasure: T is just a type variable, and you must indicate actual class, usually as Class argument. Without such information, best that can be done is to use bounds; and plain T is roughly same as 'T extends Object'. And Jackson will then bind JSON Objects as Maps.

In this case, tester method needs to have access to Class, and you can construct

JavaType type = mapper.getTypeFactory().
constructCollectionType(List.class, Foo.class)

and then

List<Foo> list = mapper.readValue(new File("input.json"), type);

How to deserialize generic ListT with Jackson?

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

  • Type Erasure
  • Type Erasure in Java Explained
  • Java generics type erasure: when and what happens?

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 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:

  • How to use Jackson's TypeReference with generics?
  • Jackson create JavaType from Class

ObjectMapper using TypeReference not working when passed type in generic method

The problem is erasure. All these <T> parameters don't exist in the compiled code, after they're erased. This means that source new TypeReference<TestPageResult<T>>() looks like new TypeReference<TestPageResult>() once compiled, which is not what you want. (Similar to how a List<String> ends up being a List in compiled code, and it's just compile-time validation that you don't add Integers to your String List.)

I think there's roughly two ways to deal with this (in this case), both of these you already stumbled upon:

  • Either you create a type that properly represents what you want, such as: new TypeReference<TestPageResult<SomeDto>>(), or class SomeDtoPageResult extends TestPageResult<SomeDto> which you can then use in places like readValue(..., SomeDtoPageResult.class);
  • Or you create a complete class representation, like you were doing with JavaType

What you really want won't work. Your best bet is to tinker and come up with the cleanest code that solves it. Generics let you express really elaborate structures, and when you serialize an actual instance (nested objects), that comes out just fine, but when the classes need to be introspected at runtime, e.g. for deserialization (your use case) or to build a model (e.g. to generate Swagger docs), this becomes problematic.

How does Jackson deserialize json into a generic type?

Your problem is here:

new TypeReference<T>()

This doesn't do what you expect it to do. Java generics are erased at runtime; therefore the above statement is basically new TypeReference<Object>.

In other words - the fact that you declared

Person<Child> readPerson = new Person<>();

to expect Child objects is simply not sufficient!

You probably have to pass the specific class Child.class to the code that maps JSON strings back. For further information, look here.



Related Topics



Leave a reply



Submit