Sometime you need to serialize a hierarchical model to JSON. Jackson offers different ways to handle the problem. Here I will expose two of them.
tl;dr;
First solution: Annotate the super class with @JsonTypeInfo(use=Id.CLASS). Pros: works with any subtype. Cons: breaks with classes or package renaming.
Second solution: Annotate the super class with:
@JsonTypeInfo(use=Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value=Cat.class, name="Cat"),
@JsonSubTypes.Type(value=Dog.class, name="Dog")
})
adding a JsonSubTypes.Type for each sub-class. Pros: no problems with renaming if custom names are provided. Cons: requires to specify all the sub-classes in the super-class.
Long story
We have animals:
public abstract class Animal {
public String name;
}
between them dogs:
public class Dog extends Animal {
public double barkVolume;
@Override
public String toString() {
return "Dog [name=" + name + ", barkVolume=" + barkVolume + "]";
}
}
and cats:
public class Cat extends Animal {
public boolean likesCream;
public int lives;
@Override
public String toString() {
return "Cat [name=" + name + ", likesCream=" + likesCream + ", lives=" + lives + "]";
}
}
We try to serialize a cat:
ObjectMapper mapper = new ObjectMapper();
Cat cat = new Cat();
cat.name = "Fuffy";
cat.likesCream = false;
cat.lives = 7;
String json = mapper.writeValueAsString(cat);
System.out.println(json);
obtaining the following JSON:
{
"name" : "Fuffy",
"likesCream" : false,
"lives" : 7
}
But when we try to deserialize it:
String json = ...
Animal expectedCat = mapper.readValue(json, Animal.class);
System.out.println(expectedCat);
we get an exception:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.github.fedy2.jhe.model.Animal: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: {"name":"Fuffy","likesCream":false,"lives":7}; line: 1, column: 1]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1456)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1012)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:149)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
at com.github.fedy2.jhe.SerializeDeserialize.main(SerializeDeserialize.java:28)
Not good. Looks like Jackson is not able to understand if the JSON refers to a cat or to a dog.
We can help Jackson annotating the super-class with the JsonTypeInfo annotation. The annotation will add type information to the generated JSON.
We can tell Jackson to use the fully-qualified Java class name as type information:
@JsonTypeInfo(use=Id.CLASS)
public abstract class Animal {
...
}
a @class property will be added with the full class name:
{
"@class" : "com.github.fedy2.jhe.model.Cat",
"name" : "Fuffy",
"likesCream" : false,
"lives" : 7
}
If you like a shorter class name you can use the Id.MINIMAL_CLASS option:
@JsonTypeInfo(use=Id.MINIMAL_CLASS)
public abstract class Animal {
...
}
a @c property will be added with a shorter class name:
{
"@c" : ".Cat",
"name" : "Fuffy",
"likesCream" : false,
"lives" : 7
}
With both solutions we should be worried about refactoring: if we change the classes or package names we will not be able to deserialized previously stored JSON.
As alternative we can store custom names using the Id.NAME option:
@JsonTypeInfo(use=Id.NAME)
public abstract class Animal {
...
}
Obtaining a JSON with a new property @type with the type name:
{
"@type" : "Cat",
"name" : "Fuffy",
"likesCream" : false,
"lives" : 7
}
By default Jackson uses the class name as name.
Unfortunately during the deserialization we get an exception:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'Cat' into a subtype of [simple type, class com.github.fedy2.jhe.model.Animal]: known type ids = [Animal]
at [Source: {"@type":"Cat","name":"Fuffy","likesCream":false,"lives":7}; line: 1, column: 10]
at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:42)
at com.fasterxml.jackson.databind.DeserializationContext.unknownTypeIdException(DeserializationContext.java:1477)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownTypeId(DeserializationContext.java:1170)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._handleUnknownTypeId(TypeDeserializerBase.java:282)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:156)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:112)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:142)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
at com.github.fedy2.jhe.SerializeDeserialize.main(SerializeDeserialize.java:28)
Jackson is not able to map the type name to a class. To solve the problem we provide sub-class information with the JsonSubTypes annotation:
@JsonTypeInfo(use=Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value=Cat.class),
@JsonSubTypes.Type(value=Dog.class)
})
public abstract class Animal {
...
}
We can use custom names specifying them in the Type annotation:
@JsonTypeInfo(use=Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value=Cat.class, name="Gatto"),
@JsonSubTypes.Type(value=Dog.class, name="Cane")
})
public abstract class Animal {
...
}
Now we get a JSON with our custom names:
{
"@type" : "Gatto",
"name" : "Fuffy",
"likesCream" : false,
"lives" : 7
}
We can obtain the same result annotating the sub-class with a JsonTypeName annotation:
@JsonTypeName("Gatto")
public class Cat extends Animal {
...
}
With custom names we can refactoring without problems but we will need to specify all the subtypes in the super class.
For more information please refer to official documentation: http://wiki.fasterxml.com/JacksonPolymorphicDeserialization