github linkedin
Jackson's Polymorphic Deserialization
2019-12-21

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 Cats or Dogs. 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.


Tags: java

Back to posts