Say you have this class hierarchy:
abstract class Animal {
String name;
}
class Cat extends Animal {
boolean canMeow;
}
class Dog extends Animal {
boolean canBark;
}
and you have a class with a List<Animal>
, which can contain Dog
and Cat
instances at runtime:
class Zoo {
List<Animal> animals;
}
and you want to serialize it as JSON with Jackson:
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
Zoo animals = new Zoo(List.of(cat, dog));
String animalsJson = mapper.writeValueAsString(animals);
then this will work out of the box and produces this JSON:
{
"animals" : [ {
"name" : "Ms. Cat",
"canMeow" : true
}, {
"name" : "Mr. Dog",
"canBark" : true
} ]
}
So far so good. Trying to read that JSON back into an Zoo
class via
Zoo animals2 = mapper.readValue(animalsJson, Zoo.class);
will throw an exception:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `de.mkammerer.jpd.Animal` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (String)"{
"animals" : [ {
"name" : "Ms. Cat",
"canMeow" : true
}, {
"name" : "Mr. Dog",
"canBark" : true
} ]
}"; line: 2, column: 17] (through reference chain: de.mkammerer.jpd.Zoo["animals"]->java.util.ArrayList[0])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1589)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1055)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:265)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:286)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:245)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)
at de.mkammerer.jpd.Main.main(Main.java:50)
If you look at the JSON, there is no way to find out if the objects in the animals
list are Cat
s or Dog
s. To solve this, you
can either write a custom deserializer or use some Jackson annotations. I’m going to show the annotations, as I find
them a very elegant solution for this problem (and the custom deserializer is more code).
// Tell Jackson to include a property called 'type', which determines what concrete class is represented by the JSON
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
// You have to list all sub-types of this class here
@JsonSubTypes({
// Maps "type": "dog" to the Dog class
@JsonSubTypes.Type(name = "dog", value = Dog.class),
// Maps "type": "cat" to the Cat class
@JsonSubTypes.Type(name = "cat", value = Cat.class)
})
abstract class Animal {
String name;
}
Now the generated JSON looks like this:
{
"animals" : [ {
"type" : "cat",
"name" : "Ms. Cat",
"canMeow" : true
}, {
"type" : "dog",
"name" : "Mr. Dog",
"canBark" : true
} ]
}
and Jackson can deserialize it back into the correct classes.
I would always use JsonTypeInfo.Id.NAME
, as embedding the class names of your code will hinder refactoring, and more
importantly, open up some nasty problems because the client can then force the server which classes to instantiate (looking at you,
Java serialization). With JsonTypeInfo.Id.NAME
you are on the safe side, as you have to whitelist the allowed subclasses.
You can even rename for example the Cat
class to something else, as long as you don’t touch the name
attribute from the
@JsonSubTypes.Type
annotation.
The working code for that blog post can be found on my GitHub.