Feature: Polymorphic Type Handling, PTH, (formerly known as "Polymorphic Deserialization")

(see Jira entry JACKSON-91 for details)

Polymorphic type handling here means ability to enable addition of enough type information so that deserializer can instantiate correct subtype of a value, even if declaration of the field/setter/creator method only has single type (supertype) defined. So, for example, type Zoo below can be serialized AND deserialized properly:

  public class Zoo {
    public Animal animal;
  }

  static class Animal { // All animals have names, for our demo purposes...
     public String name;
     protected Animal() { }
  }

  static class Dog extends Animal {
    public double barkVolume; // in decibels
    public Dog() { }
  }

  static class Cat extends Animal {
    boolean likesCream;
    public int lives;
    public Cat() { }
  }

so that even though deserializer only knows statically that Zoo contains an Animal, serializer can be instructed to embed additional information, to let deserializer know whether we have a Cat or Dog instance.

How? Glad you asked....

1. Usage

First: there are actually two complementary ways to resolve this problem. Which one to choose depends on multiple factors; but let's first consider methods.

1.1. Global default typing

First, you can globally declare that certain types always require additional type information:

  // one of:
  objectMapper.enableDefaultTyping(); // default to using DefaultTyping.OBJECT_AND_NON_CONCRETE
  objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

what this means is that for all types specified (for no-args, "Object.class" and all non-final classes), certain amount of default type information (Java class name, more specifically), is included, using default inclusion mechanism (additional wrapper array in JSON). This global default can be overridden by per-class annotations (more on this in next section).

The only thing you can configure, then, is just which types (classes) are affected. Choices are:

This is often simplest initial way to enable enough type information to get things going.

It is also possible to customize global defaulting, using ObjectMapper.setDefaultTyping(...) -- you just have to implement your own TypeResolverBuilder (which is not very difficult); and by doing so, can actually configure all aspects of type information. Builder itself is just a short-cut for building actual handlers.

1.2. Per-class annotations

More granular (and powerful) way to define what type information to add, and where, is to use @JsonTypeInfo annotation (and possibly couple of related ones). For example, we could have annotated Animal as follows:

 @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="@class")
 class Animal { } 

(which, incidentally is equivalent to the default settings for typing).

What does that mean?

We could have chosen differently as follows:

Finally: it is also possible to use JAXB annotations to indicate need for adding type information (see @XmlElements for details).

2. On type ids

Type ids that are based on Java class name are fairly straight-forward: it's just class name, possibly some simple prefix removal (for "minimal" variant). But type name is different: one has to have mapping between logical name and actual class. This relationship is defined by:

Alternatively, you can also use JAXB annotations (specifically, @XmlElements) to establish type names; as well as need to included type information.

In future we may want to add additional methods for linking types with sub-types: current method is not optimal for use cases where subtypes may be added dynamically; and it does add unnecessary back-links between types (even if as annotation metadata).

3. Additional thoughts

Back to choosing between global default typing, and explicit annotations. Which one should I choose?

4. On Design

For those interested in actual progress from basic desire ("should be able to serialize any List of Objects") into implementation -- especially one that is quite complicated -- it may be interesting to read through original design notes. Here you go...

4.1. Definition of PTH

Polymorphism is an Object-Oriented Design concept that Java implements by class inheritance. Here it just means ability to construct instances of sub-classes of a given declared class, based on which sub-class was actually serialized. That is, even though during serialization the declared type is a super-type, it should be possible for the deserializer to properly resolve actual expected type of the value to assign.

Implementing support for polymorphic types is one of highest priority items for Jackson development. At the same time, implementation that covers require use cases (including cases where not all communicating systems run on Java platform) is not trivially simple to implement, so care has to be taken to create a simple, powerful and extensible design. To come up with such a design, let's first consider type design choices and alternatives that are available.

4.2. Design Choices

Instance Type Information (Type Metadata)

(aka "Type Id")

To be able to deserialize JSON object into types that instances were serialized from (and not just statically declared type, which is generally a supertype), some amount of per-instance type information is needed. There are multiple possible ways to do this. For example:

To give an idea of possible concrete examples of such instance type information, here are some examples (but please note that other choices discussed below would change actual mechanism of including type information):

  { // Using fully-qualified path
    "@class" : "com.fasterxml.beans.EmployeeImpl", ...
  }

  { // Using indirect type name
    "@type" : "Employee", ...
  }

  { // Fancy custom type information (can bind JSON object to a type object)
    "customType" : { "xmlType" : "http://foo.bar/schema.xsd", "preferredClass" : "com.foo.EmployeeClass" },
    ...
  }

Different mechanisms have different trade-offs; for example:

Methods for embedding Instance Type Information

(aka "Include As")

After deciding what type information (usually a Type Id of some kind) to add along with instance data, it is necessary to define how this information is to be included in actual JSON data. Some obvious methods of inclusion are:

Given examples from previous section, here are possible ways to embed class name information:

  // Type name as a property, same as above
  {
    "@type" : "Employee",
     ...
  }

  // Wrapping class name as pseudo-property (similar to JAXB)
  {
    "com.fasterxml.beans.EmployeeImpl" : {
       ... // actual instance data without any metadata properties
    }
  }

  // Wrapping class name as first element of wrapper array:
  [
    "com.fasterxml.beans.EmployeeImpl",
    {
       ... // actual instance data without any metadata properties
    }
  ]

4.3 Customizing type handling

Here are some aspects that should be customizable:

4.4. Implementation Plan

Current thinking is to support multiple Type Metadata methods as well as multiple inclusion methods. And since these are somewhat orthogonal, allow different combinations of the two. And finally make aspects configurable with sensible defaults, to try to fulfill sometimes conflicting goals of simplicity (as close to "zero configuration" as possible) and configurability (ability to customize behavior to exact needs and preferences).

As with other configurability for Jackson, emphasis will initially be on allowing configuration using Java Annotations. Benefits include some level of type-safety, as well as flexibility when using Annotation Mix-ins.

4.4.1 Baseline: global defaults

In addition to per-type definitions by annotations, it is necessary to allow setting of global baseline. The main reason is convenience: although it may be technically possible to annotate all possible types (or might not...), it may be unfeasible for practical purposes. As such it is good to be able to define a default type handling baseline, different from default of "no type information".

4.4.2 Per-type annotations

All annotations that do NOT depend on mapper types, will live in the main org.codehaus.jackson.annotate package. Other annotations (with dependencies) will live in org.codehaus.jackson.map.annotate package.

The main annotation used for indicating type is @JsonTypeInfo. It has following properties:

5. Known Issues

5.1 Missing type information on Serialization

One common problem occurs with cases like this:

  ObjectMapper mapper = new ObjectMapper();
  List<MyPojo> pojos = new ArrayList<MyPojo>();
  // add entries; MyPojo is a type with @JsonTypeInfo
  pojos.add(new MyPojoImpl(...));
  String json = mapper.writeValueAsString(pojos);

where contents of 'json' may not include any type information.

Why? Because of so-called Type Erasure (see Java Generics FAQ for more information), which means that instance of

  List<MyPojo> pojos

really only has effective runtime type of:

  List<?> pojos; // which is about same as 'List<Object>'

and thereby Jackson can only base its introspection on what type java.lang.Object has.

There are multiple ways to solve this:

Last method is the most flexible, and it will force actual type used for serialization, affecting both fields that get serialized and type used for introspection (note: this may occasionally be problematic, since it means that information present only in subtypes would not be included).

For above case, any of these would work:

   objectMapper.writerWithType(new TypeReference<List<MyPojo>>() { }).writeValueAsString(); // etc
   JavaType rootType = objectMapper.constructCollectionType(List.class, MyPojo.class);
   objectMapper.writerWithType(type).writeValueAsString(); // etc


CategoryJackson

JacksonPolymorphicDeserialization (last edited 2011-12-02 22:26:47 by TatuSaloranta)

Copyright ©2009 FasterXML, LLC