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