Handling polymorphism with Jackson

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