Close

Jackson JSON - Using @JsonTypeInfo annotation to handle polymorphic types

[Last Updated: Aug 11, 2020]

In cases where polymorphic types are persisted to JSON, there's no way for Jackson to figure out the right type during deserialization. Let's understand that with an example.

public abstract class Shape {
}
public class Rectangle extends Shape {
  private int w;
  private int h;
    .............
}
public class Circle extends Shape {
  int radius;
    .............
}
public class View {
  private List<Shape> shapes;
    .............
}

Let's serialize and then deserialize a View object:

public class ExampleMain {
  public static void main(String[] args) throws IOException {
      View v = new View();
      v.setShapes(new ArrayList<>(List.of(Rectangle.of(3, 6), Circle.of(5))));

      System.out.println("-- serializing --");
      ObjectMapper om = new ObjectMapper();
      String s = om.writeValueAsString(v);
      System.out.println(s);

      System.out.println("-- deserializing --");
      View view = om.readValue(s, View.class);
      System.out.println(view);
  }
}
-- serializing --
{"shapes":[{"w":3,"h":6},{"radius":5}]}
-- deserializing --
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.logicbig.example.Shape` (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)"{"shapes":[{"w":3,"h":6},{"radius":5}]}"; line: 1, column: 12] (through reference chain: com.logicbig.example.View["shapes"]->java.util.ArrayList[0])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1451)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1027)
	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.MethodProperty.deserializeAndSet(MethodProperty.java:127)
	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:4013)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
	at com.logicbig.example.ExampleMain.main(ExampleMain.java:18)

Using @JsonTypeInfo

This annotation is used to serialize information about actual class of polymorphic instances, so that Jackson can know what subtype is to be deserialized. Let's fix about exception by using this annotation:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "className")
public abstract class Shape {
}

Above configuration specifies that the qualified class name should be used (use = JsonTypeInfo.Id.CLASS) and persist that as JSON property (include = JsonTypeInfo.As.PROPERTY). The property name should be 'className'.
Let's run our main method again:

-- serializing --
{"shapes":[{"className":"com.logicbig.example.Rectangle","w":3,"h":6},{"className":"com.logicbig.example.Circle","radius":5}]}
-- deserializing --
View{shapes=[Rectangle{w=3, h=6}, Circle{radius=5}]}

In above configuration if we skip optional elements, 'include' and 'property', then defaults will be used. The default 'include' is also JsonTypeInfo.As.PROPERTY and default 'property' is @class. So our example will be:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public abstract class Shape {
}

which will be serialized as:

{"shapes":[{"@class":"com.logicbig.example.Rectangle","w":3,"h":6},{"@class":"com.logicbig.example.Circle","radius":5}]}

Using @JsonTypeInfo on properties

@JsonTypeInfo annotation can be used both on classes (above example) and properties. In our example using the annotation on 'shapes' property:

 public class View {
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "className")
    private List<Shape> shapes;
    .....
} 

If this annotation exists on both class and property, then the one on property has precedence, as it is considered more specific.

When used on properties (fields, methods), this annotation applies to values. That means when used on collection types (Collection, Map, arrays) it will apply to the elements, not the collection itself. For non-collection types there is no difference. In above snippet when we used it on 'shapes' list, it is applied on each element (Shape) of the List rather than List type itself.

Using JsonTypeInfo.Id.MINIMAL_CLASS

use = JsonTypeInfo.Id.MINIMAL_CLASS option will serialize minimal relative package path. Check out complete example here.

Using ObjectMapper.enableDefaultTyping()

This method can be used to enable global automatic inclusion of type information in cases where polymorphic types are used.

Let's use this method in our example:

public class ExampleMain2 {
  public static void main(String[] args) throws IOException {
      View v = new View();
      v.setShapes(new ArrayList<>(List.of(Rectangle.of(3, 6), Circle.of(5))));

      System.out.println("-- serializing --");
      ObjectMapper om = new ObjectMapper();
      om.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, JsonTypeInfo.As.PROPERTY);

      String s = om.writeValueAsString(v);
      System.out.println(s);

      System.out.println("-- deserializing --");
      View view = om.readValue(s, View.class);
      System.out.println(view);
  }
}
-- serializing --
{"shapes":["java.util.ArrayList",[{"@class":"com.logicbig.example.Rectangle","w":3,"h":6},{"@class":"com.logicbig.example.Circle","radius":5}]]}
-- deserializing --
View{shapes=[Rectangle{w=3, h=6}, Circle{radius=5}]}

In above example DefaultTyping.OBJECT_AND_NON_CONCRETE specifies that default typing will be used for properties with declared type of java.lang.Object or an abstract type (abstract class or interface, Shape in our example).

Note that in above output the actual type of List<Shape> is also persisted as ArrayList. That's because List is also a non-concrete type.

Example Project

Dependencies and Technologies Used:

  • jackson-databind 2.9.6: General data-binding functionality for Jackson: works on core streaming API.
  • JDK 10
  • Maven 3.3.9

@JsonTypeInfo Example Select All Download
  • jackson-json-type-info-annotation
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • Shape.java

    See Also