Jackson - Deserialize Using Generic Class

How to deserialize generic List T 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

Jackson - deserialize json into generic type

When you deserialize a JSON to a generic class, Jackson cannot guess the generic type used since that is not an information present in the JSON.

And when it doesn't know how to deserialize a field, it uses a java.util.LinkedHashMap as target.

What you want is a generic type such as :

InputMessage<KeyData> inputMessage = ...;
KeyData keyData = inputMessage.getKeyData();

An elegant way to solve that is defining a Jackson JavaType for the class by specifying the expected generic.

TypeFactory.constructParametricType(Class parametrized, Class... parameterClasses) allows that.

Supposing you want to deserialize to InputMessage<KeyData>, you can do :

JavaType type = mapper.getTypeFactory().constructParametricType(InputMessage.class, KeyData.class);
InputMessage<KeyData> keyData = mapper.readValue(json, type);

About your comment :

The library code with the generic type knows nothing about the
KeyData class, so I assume it belongs in the client code?

The library doesn't need to know this class but clients should however pass the class to perform correctly the deserialization and to return a generic instance to the client and not a raw type.

For example, clients could use the library in this way :

InputMessage<KeyData> inputMessage = myJsonLibrary.readValue("someValueIfNeeded", KeyData.class);

How to deserialize a generic type with jackson?

The simplest option would be to use a wrapper type with a separate field per collection type, like that:

class GenericEntity {
@JsonProperty("Car") List<Car> car;
@JsonProperty("Person") List<Person> person;
}

This way you would always have one of those lists filled (according to our conversation in comments). This will work fine as long as you don't have too many types and it doesn't change too frequently :)

The more-advanced way would be to use a custom deserializer, like that:

@JsonDeserialize(using = MyDeserializer.class)
class GenericEntity<T> {
List<T> myList;

GenericEntity(List<T> myList) {
this.myList = myList;
}
}

The deserializer itself would have to create a GenericEntity on its own, but it can delegate all specific-type-deserializing job to other deserializers (so our job would be just to tell it what to deserialize and to what type):

class MyDeserializer extends JsonDeserializer<GenericEntity<?>> {

@Override
public GenericEntity<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectCodec codec = p.getCodec();
JsonNode node = codec.readTree(p);
if (node.hasNonNull("Person")) {
JsonParser nodeParser = node.get("Person").traverse(codec);
nodeParser.nextToken();
Person[] people = ctxt.readValue(nodeParser, Person[].class);
return new GenericEntity<>(asList(people));
} else if (node.hasNonNull("Car")) {
JsonParser nodeParser = node.get("Car").traverse(codec);
nodeParser.nextToken();
Car[] cars = ctxt.readValue(nodeParser, Car[].class);
return new GenericEntity<>(asList(cars));
}
throw new RuntimeException("Couldn't find a type to deserialize!");
}
}

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.

How to deserialize nested generic class using Jackson Databind?

If you like to write a general method to deserialize nested generic class using Jackson Databind, you can use TypeReference Object like this :

public <T> T deserialize(Stream inStream, TypeReference<T> typeReference){
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(inStream, typeReference);
}

and in your case you can use it like this:

Foo<Bar> response = deserialize(inStream, new TypeReference<Foo<Bar>>(){});

So by TypeReference Object you can write a general method and you can pass your generic class and your inner class by TypeReference.

If you like to read more about it you can find some sample about TypeReference in this link:

https://www.programcreek.com/java-api-examples/org.codehaus.jackson.type.TypeReference

Cannot deserialize generic class hierarchy using Jackson

We ended up adding another key/value pair in the JSON – the contract was indeed modified a little bit to accommodate that "private" key/value pair.

In any case, if somebody runs into the same issue, this is the solution for the approach:

...
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties("_type")
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "_type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Cancel.class, name = "_type"),
@JsonSubTypes.Type(value = Fail.class, name = "_type"),
@JsonSubTypes.Type(value = Success.class, name = "_type"),
})
public abstract class AbstractModel {
private String userId;

private Type type;

AbstractModel() { }

AbstractModel(final String userId, final Type type) {
this.userId = userId;
this.type = type;
}

// getters, toString, etc.

public enum Type {
CANCEL("event.cancelled"),
FAIL("event.failed"),
SUCCESS("event.success");

private final String value;

Type(String value) {
this.value = value;
}

public String getValue() { return value; }
}
}


Related Topics



Leave a reply



Submit