2011-05-25

Deserialize JSON with Jackson into Polymorphic Types - A Complete Example

Jackson API Versions Note

The following code examples were written using Jackson 1.7.x or 1.8.x. They probably work as-is with Jackson 1.9.x, but Jackson 2.0 introduced API changes significant enough that this code does not compile with it. On initial review, it appears that package names changed, and at least one method signature changed or was moved or was renamed or no longer exists. In the near future, I may post updated code examples that use Jackson 2.x.

tl;dnr

Skip to the fourth, fifth, and sixth examples.

Why This Post Exists

While recently answering a question on StackOverflow.com about deserialization of JSON into polymorhpic types in Java, I couldn't find a simple and complete example using Jackson. At the time of this writing, the official documentation on the subject describes aspects of how to do this, but it doesn't have a full example. Searching other resources also didn't turn up any complete examples.
Link To This ArticleQR Code Link To This Articlehttp://goo.gl/nVKBZ

Following are deserialization examples with Jackson that build up to a complete example of populating a polymorphic data structure in Java. The first three examples do not involve Polymorphism. Polymorphism is introduced in the fourth example.

Consult the Jackson API documentation for explanations of each component.

Example 1: Simple Object Deserialization and Serialization

// input and output:
// {"type":"dog","name":"Spike"}

import org.codehaus.jackson.map.ObjectMapper;

public class Foo
{
static String jsonInput =
"{\"type\":\"dog\",\"name\":\"Spike\"}";

public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
Animal animal = mapper.readValue(jsonInput, Animal.class);
System.out.println(mapper.writeValueAsString(animal));
}
}

class Animal
{
public String type;
public String name;
}

Example 2: Simple Collection Deserialization and Serialization

// input and output:
// [
// {"type":"dog","name":"Spike"},
// {"type":"cat","name":"Fluffy"}
// ]

import java.io.File;
import java.util.Collection;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.TypeReference;

public class Foo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
Collection<Animal> animals =
mapper.readValue(new File("input_2.json"),
new TypeReference<Collection<Animal>>() {});
System.out.println(mapper.writeValueAsString(animals));
// or
Collection<Animal> animals2 =
mapper.readValue(new File("input_2.json"),
TypeFactory.defaultInstance().constructParametricType(
Collection.class, Animal.class));
System.out.println(mapper.writeValueAsString(animals2));
}
}

class Animal
{
public String type;
public String name;
}

Example 3: Simple Deserialization/Serialization To/From Container Object With Collection

// input and output:
// {
// "animals":
// [
// {"type":"dog","name":"Spike"},
// {"type":"cat","name":"Fluffy"}
// ]
// }

import java.io.File;
import java.util.Collection;

import org.codehaus.jackson.map.ObjectMapper;

public class Foo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
Zoo zoo =
mapper.readValue(new File("input_3.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}

class Zoo
{
public Collection<Animal> animals;
}

class Animal
{
public String type;
public String name;
}

Example 4: Simple Deserialization/Serialization To/From Container Object With Polymorphic Collection

// input and output:
// {
// "animals":
// [
// {"type":"dog","name":"Spike","breed":"mutt",
// "leash_color":"red"},
// {"type":"cat","name":"Fluffy",
// "favorite_toy":"spider ring"}
// ]
// }

import java.io.File;
import java.util.Collection;

import org.codehaus.jackson.annotate.JsonSubTypes;
import org.codehaus.jackson.annotate.JsonSubTypes.Type;
import org.codehaus.jackson.annotate.JsonTypeInfo;
import org.codehaus.jackson.map.ObjectMapper;

public class Foo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
Zoo zoo =
mapper.readValue(new File("input_4.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}

class Zoo
{
public Collection<Animal> animals;
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@Type(value = Cat.class, name = "cat"),
@Type(value = Dog.class, name = "dog") })
abstract class Animal
{
public String name;
}

class Dog extends Animal
{
public String breed;
public String leashColor;
}

class Cat extends Animal
{
public String favoriteToy;
}
import org.codehaus.jackson.map.MapperConfig;
import org.codehaus.jackson.map.PropertyNamingStrategy;
import org.codehaus.jackson.map.introspect.AnnotatedField;
import org.codehaus.jackson.map.introspect.AnnotatedMethod;

/**
* Converts standard CamelCase field and method names to
* typical JSON field names having all lower case characters
* with an underscore separating different words. For
* example, all of the following are converted to JSON field
* name "some_name":
*
* Java field name "someName"
* Java method name "getSomeName"
* Java method name "setSomeName"
*
* Typical Use:
*
* String jsonString = "{\"foo_name\":\"fubar\"}";
* ObjectMapper mapper = new ObjectMapper();
* mapper.setPropertyNamingStrategy(
* new CamelCaseNamingStrategy());
* Foo foo = mapper.readValue(jsonString, Foo.class);
* System.out.println(mapper.writeValueAsString(foo));
* // prints {"foo_name":"fubar"}
*
* class Foo
* {
* private String fooName;
* public String getFooName() {return fooName;}
* public void setFooName(String fooName)
* {this.fooName = fooName;}
* }
*/
public class CamelCaseNamingStrategy
extends PropertyNamingStrategy
{
@Override
public String nameForGetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName)
{
return translate(defaultName);
}

@Override
public String nameForSetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName)
{
return translate(defaultName);
}

@Override
public String nameForField(MapperConfig<?> config,
AnnotatedField field, String defaultName)
{
return translate(defaultName);
}

private String translate(String defaultName)
{
char[] nameChars = defaultName.toCharArray();
StringBuilder nameTranslated =
new StringBuilder(nameChars.length * 2);
for (char c : nameChars)
{
if (Character.isUpperCase(c))
{
nameTranslated.append("_");
c = Character.toLowerCase(c);
}
nameTranslated.append(c);
}
return nameTranslated.toString();
}
}

Example 5: Simple Deserialization/Serialization With MixIn To/From Container Object With Polymorphic Collection

// input and output:
// {
// "animals":
// [
// {"type":"dog","name":"Spike","breed":"mutt",
// "leash_color":"red"},
// {"type":"cat","name":"Fluffy",
// "favorite_toy":"spider ring"}
// ]
// }

import java.io.File;
import java.util.Collection;

import org.codehaus.jackson.annotate.JsonSubTypes;
import org.codehaus.jackson.annotate.JsonTypeInfo;
import org.codehaus.jackson.annotate.JsonSubTypes.Type;
import org.codehaus.jackson.map.ObjectMapper;

public class Foo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
mapper.getDeserializationConfig().addMixInAnnotations(
Animal.class, PolymorphicAnimalMixIn.class);
mapper.getSerializationConfig().addMixInAnnotations(
Animal.class, PolymorphicAnimalMixIn.class);
Zoo zoo =
mapper.readValue(new File("input_5.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}

class Zoo
{
public Collection<Animal> animals;
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@Type(value = Cat.class, name = "cat"),
@Type(value = Dog.class, name = "dog") })
abstract class PolymorphicAnimalMixIn
{

}

abstract class Animal
{
public String name;
}

class Dog extends Animal
{
public String breed;
public String leashColor;
}

class Cat extends Animal
{
public String favoriteToy;
}

Example 6: Simple Deserialization Without Type Element To Container Object With Polymorphic Collection


Some real-world JSON APIs have polymorphic type members, but don't include type elements (unlike the JSON in the previous examples). Deserializing such sources into polymorphic collections is a bit more involved. Following is one relatively simple solution. (This example includes subsequent serialization of the deserialized Java structure back to input JSON, but the serialization is relatively uninteresting.)
// input and output:
// {
// "animals":
// [
// {"name":"Spike","breed":"mutt","leash_color":"red"},
// {"name":"Fluffy","favorite_toy":"spider ring"},
// {"name":"Baldy","wing_span":"6 feet",
// "preferred_food":"wild salmon"}
// ]
// }

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.deser.StdDeserializer;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ObjectNode;

import fubar.CamelCaseNamingStrategy;

public class Foo
{
public static void main(String[] args) throws Exception
{
AnimalDeserializer deserializer =
new AnimalDeserializer();
deserializer.registerAnimal("leash_color", Dog.class);
deserializer.registerAnimal("favorite_toy", Cat.class);
deserializer.registerAnimal("wing_span", Bird.class);
SimpleModule module =
new SimpleModule("PolymorphicAnimalDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Animal.class, deserializer);

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
mapper.registerModule(module);

Zoo zoo =
mapper.readValue(new File("input_6.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}

class AnimalDeserializer extends StdDeserializer<Animal>
{
private Map<String, Class<? extends Animal>> registry =
new HashMap<String, Class<? extends Animal>>();

AnimalDeserializer()
{
super(Animal.class);
}

void registerAnimal(String uniqueAttribute,
Class<? extends Animal> animalClass)
{
registry.put(uniqueAttribute, animalClass);
}

@Override
public Animal deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Class<? extends Animal> animalClass = null;
Iterator<Entry<String, JsonNode>> elementsIterator =
root.getFields();
while (elementsIterator.hasNext())
{
Entry<String, JsonNode> element=elementsIterator.next();
String name = element.getKey();
if (registry.containsKey(name))
{
animalClass = registry.get(name);
break;
}
}
if (animalClass == null) return null;
return mapper.readValue(root, animalClass);
}
}

class Zoo
{
public Collection<Animal> animals;
}

abstract class Animal
{
public String name;
}

class Dog extends Animal
{
public String breed;
public String leashColor;
}

class Cat extends Animal
{
public String favoriteToy;
}

class Bird extends Animal
{
public String wingSpan;
public String preferredFood;
}
FINI

37 comments:

  1. Awesome, exactly what I needed...may want to put a little more description on each example but thanks!

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Excellent article. Have you ever run into the situation like Example 5, but you have multiple types pointing to one parent class. I think I found a bug in Jackson, because I tried two types pointing to the same class and it ignores the second one.
    package com.blogspot.programmerbruce.jackson;

    // input and output:
    // {"beings": [
    // {
    // "type": "plant",
    // "name": "pete"
    // },
    // {
    // "type": "guppie",
    // "name": "gus",
    // "bearing": "live"
    // },
    // {
    // "type": "angelfish",
    // "name": "angie",
    // "bearing": "egg"
    // }
    // ]}

    import java.io.File;
    import java.util.Collection;

    import org.codehaus.jackson.annotate.JsonSubTypes;
    import org.codehaus.jackson.annotate.JsonSubTypes.Type;
    import org.codehaus.jackson.annotate.JsonTypeInfo;
    import org.codehaus.jackson.map.ObjectMapper;

    public class Example5_2 {
    public static void main(String[] args) throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setPropertyNamingStrategy(
    new CamelCaseNamingStrategy());

    mapper.getDeserializationConfig().addMixInAnnotations(Being.class, PolymorphicBeingMixIn.class);
    mapper.getSerializationConfig().addMixInAnnotations(Being.class, PolymorphicBeingMixIn.class);

    File file = new File("./test/com/blogspot/programmerbruce/jackson/input_5_2.json");
    Fishtank fishtank =
    mapper.readValue(file, Fishtank.class);
    System.out.println(mapper.writeValueAsString(fishtank));
    }
    }

    class Fishtank {
    public Collection beings;
    }

    @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type")
    @JsonSubTypes({
    // !!!
    // TRY ALTERNATING THIS, and alternatively, the first mapping will work, but the second fails.
    // !!!
    @Type(value = Fish.class, name = "angelfish"),
    @Type(value = Fish.class, name = "guppie"),
    @Type(value = Plant.class, name = "plant") })
    abstract class PolymorphicBeingMixIn {
    }

    abstract class Being {
    public String name;
    }

    class Fish extends Being {
    public String bearing;
    }

    class Plant extends Being {
    }

    ReplyDelete
  4. You helped me to solve 2 issues :).
    Thanks

    ReplyDelete
  5. Is there a way to serialize/deserialize as in Example 5 but w/out using annotations?

    ReplyDelete
  6. For the interested, I logged Jackson issue 728 for an enhancement to provide non-annotation-based subtype registration. Don't hesitate to vote for the issue if you want it implemented.

    ReplyDelete
  7. If you add constructors to example 6 it no longer works. There is a unable to find suitable constructor error. I wish to have immutable classes so making all parameters final is required. I can easily know the class but that doesn't help.

    ReplyDelete
  8. Adding Mixin classes seems to solve the problem for me.

    ReplyDelete
  9. Thanks. Te deszerialization with polymorphism makes my day!

    ReplyDelete
  10. Yes, deserialization with polymorphism is the exact fit, sweet!

    ReplyDelete
  11. How do I deal with classes that are available to me as jar files with no source code provided?

    Example: if the source code of the Animal class file is not available, how do I annotate the class with Jackson annotations?

    @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type")
    @JsonSubTypes({
    @Type(value = Cat.class, name = "cat"),
    @Type(value = Dog.class, name = "dog") })

    This article is really excellent, but I am wondering if there are ways to configure the annotations properties at runtime to the classes I am dealing with.

    ReplyDelete
    Replies
    1. I'm another new Jackson user, and I'm looking for a way to do this also.

      Delete
    2. If you have no control over the classes you need to serialize/deserialize, example 6 is the way to go. Write your own deserializer and infer the classes to bind to from the json attributes.

      Delete
    3. You may want to use "mix-in annotations" -- these allow associating annotation values without modifying target classes.

      Delete
  12. Thank you for taking the time to write up the excellent examples.

    ReplyDelete
  13. This is an excellent example, and it's really helped me what I'm trying to do.

    I've extended example 5 to resolve the JSON coming from a database field into the appropriate object and produced a suite of tests so that I'm satisfied it's reading and writing them correctly.

    My problem is that the JSON data is only a single field and I want to serialize the entire row into JSON to provide it to a feed.

    If I don't use the mapper and allow the Jackson default parsing to serialize the object, then the type element is lost when I examine the output from the feed. However, if I do use the mapper, I get the type element, but the entire JSON output from that is treated as a string in the JSON in the feed and is escaped as such.

    I don't want to have to do any post processing in the back end or preprocessing in the front end to overcome this.

    Suggestions?

    ReplyDelete
  14. For questions on using Jackson, I recommend asking at stackoverflow.com.

    ReplyDelete
  15. Example 6 works great if the base class is abstract. But what happens if the base class itself is not abstract and can be instantiated?
    You can simply conclude that it is actually the base class because it does NOT have any of the registered attributes.
    But the line

    return mapper.readValue(root, animalClass);

    would then result in an endless loop. How can I resolve this if adding an additional abstract base class is not possible?

    ReplyDelete
  16. Awesome article! Really saved me some time :)

    ReplyDelete
  17. Great article!

    Adding the following code to example 4:
    JsonSchema jsonSchema = mapper.generateJsonSchema(Zoo.class);
    System.out.println(mapper.writeValueAsString(jsonSchema));

    I get:
    {
    "type" : "object",
    "properties" : {
    "animals" : {
    "type" : "array",
    "items" : {
    "type" : "any"
    }
    }
    }
    }
    that is, the schema says that "Zoo.animals" is a collection of objects,
    rather than a collection of Animals.

    Is that a limitation of Jackson, or a limitation of json-schema.org?

    Any idea how to solve this?

    Many thanks,
    David

    ReplyDelete
  18. This is just great, thank you so much!

    Onur

    ReplyDelete
  19. Normally I not tend to leave commits like "thank you" or "great article", but I must admit that your writeup impressed me.

    Thank you! Great article!
    I really impressed, you helped me much!

    Cheers,

    T

    ReplyDelete
  20. I was looking for a solution that would deduce the specific type based on fields included in the serialized data and found it here in example 6. Thanks a lot!

    ReplyDelete
  21. A big thanks for this article - I was struggling on solution 4 (Polymorphic fields) for a couple of hours without finding any relevant/good examples on google! I've copied the demo to my project & works like a charm.

    A big thanks!

    ReplyDelete
  22. Wow, took me awhile to find this article, but it's just what I needed! Thanks!

    ReplyDelete
  23. Thanks for taking the time to do this writeup it helped me a lot!

    ReplyDelete
  24. Thanks for taking the time to post this, very useful, saved a lot of my time!!!

    ReplyDelete