Saturday, June 8, 2013

JSON Polymorphic Types with Jackson - Problem and its solution

Motivation:

While searching the web for a solution for my problem, I didn't find one. Searching Google, Bing and StackOverFlow didn't return a complete solution. Because of the strange problem I got, I should search again and again. After seeing a lot of topics talk about JSON Polymorphic Types and JSON documentation, I solved the problem and see it is useful for all of you to know the solution. I believe it is a general solution, so I decided to share it with you.

Problem:

My task was to make a Client API to "X-Service". this Client API will be coded in Java and will take a JSON String and deserialize it. The problems were that: 1) I don't know the manner that this JSON String written by. 2) The JSON String contains Polymorphic Types. 3) The Polymorphic Types don't have a clear unique identifier to know which class to assign while deserilaization process. But I can make an unique identifier that will consist of two fields.

Note: "X-Service" is not the original name. The names changed due to non-disclosure.

Explanation:

I'll go through an explanation with an example of the problem. I'll provide a general example and solution for Polymorphic Types.

The following code example shows Polymorphic Types. You can see this in two classes Cat and Dog and both extends Animal class. The solution is in @JsonTypeInfo and @JsonSubTypes notations.
// 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("inputFile.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;
}

This is due to the default Jackson deserializer will use @JsonTypeInfo and @JsonSubTypes notations to deserialize the JSON String into the right polymorphic objects. But, What is the solution if the input JSON String is:

// input and output:
//   {
//     "animals":
//     [
//       {"type":"pet","subType":"dog","name":"Spike"},
//       {"type":"other","name":"any name"},
//       {"type":"pet","subType":"cat","name":"Fluffy"},
//       {"type":"wild","subType":"lion", "name":"Layth"}
//     ]
//   }
The problem here is that we have new data structures and none of the fields can be annotated as an unique identifier for Polymorphic Types. The new data structure are below.
abstract class Animal
{
  public String name;
}

class Dog extends Animal
{
  public String type = "pet";
  public String subType = "dog";
}

class Cat extends Animal
{
  public String type = "pet";
  public String subType = "cat";
}

class Lion extends Animal
{
  public String type = "wild";
  public String subType = "lion";
}

class OtherAnimals extends Animal
{
  public String type = "other";
}

You may see this strange!! Me too :) Just proceed with the task as your manager said. Don't ask why he made data structures this manner :).

First Step: write a custom serializer class in Jackson manner and embed your logic that will distinguish between polymorphic types in it like this:

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 uniqueAttribute1, String uniqueAttribute2
            ,Class<? extends Animal> animalClass) {
        String str = uniqueAttribute1;
        if(uniqueAttribute2 != null)
            str = str + ":" + uniqueAttribute2;
        registry.put(str, 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();
        String Type = null;
        String SubType = null;
        while (elementsIterator.hasNext()) {
            Entry<String, JsonNode> element = elementsIterator.next();
            String name = element.getKey();      
            if(name.equals("Type"))
                Type = element.getValue().asText();
            else if(name.equals("SubType"))
                SubType = element.getValue().asText();
        }
        String str = Type;
        if (SubType != null) {
            str = str + ":" + SubType;
        }
        if (registry.containsKey(str)) {
            animalClass = registry.get(str);
        }
        if (animalClass == null) {
            return null;
        }
        return mapper.readValue(root, animalClass);
    }
}
In this class I noticed that "type" and "subType" fields made together a unique identifier. I exploits this feature and embed my logic in the new serializer as in the above class . and in the following code I'll add a SimpleModule that will contain my customized deserializer to register a unique identifiers for each class extends Animal class.
public static void main(String[] args) throws Exception  
  {
AnimalDeserializer  deserializer = new AnimalDeserializer ();
deserializer.registerAPIResultWidget("pet", "dog", Dog.class);
deserializer.registerAPIResultWidget("pet", "cat", Cat.class);
deserializer.registerAPIResultWidget("wild", "lion", Lion.class);
deserializer.registerAPIResultWidget("other", null, OtherAnimal.class);
SimpleModule module = new SimpleModule("PolymorphicAnimalDeserializerModule",
                new Version(1, 0, 0, null));
module.addDeserializer(Animal.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module); 
Zoo zoo = mapper.readValue(new File("inputFile.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
  }
Using this style in thinking in the problem, you can generalize this solution in Polymorphic Types using Jackson.

2 comments: