Java – Jackson polymorphic deserialization with dynamic types

Jackson polymorphic deserialization with dynamic types… here is a solution to the problem.

Jackson polymorphic deserialization with dynamic types

I have a data structure with some strongly typed fields and some loosely typed fields. Some of these fields are collections that can be nested arbitrarily deeply.

Example JSON

{
  "prop": "Hello",              //strongly-typed
  "child1": {
    "anInt": -1
  },
  "map": {                      // here magic begins
    "JustString": "JustValue",  // we may store any Object in this map
    "Item_With_Type": {
      "@type": "MyMap",         // some of them tell their type and we need to rely on it
      "Custom": "Value"
    },
    "List_With_All_Child1": {
      "@type": "MyMap[]",       // lists define the type of all values in it in this way
      "@values": [
        {
          "Key": "Value",       // MyMap is a Map
          "Child1": {           // of <? extends Object>
             "anInt": 2
           }
        },
        {
          "Key": "Value"
        }
      ]
    }
  }
}

I want to map this to

public static class Parent {
    private String prop;
    private Child1 child1;
    private MyMap<?> map;
}

public static class Child1 {
    private int anInt;
}

public static class MyMap<T> extends HashMap<String, T> implements Map<String, T> {
}

(Omit accessor).

Basically I need a data binding (bind) that Jackson asks for every time he tries to save a context-resolved type for any field, and if this bind-bind doesn’t find anything specific to the application, Jackson should fall back to the default type resolution.

Are there any ideas to make it happen?

Solution

After playing with Jackson for a while, I came up with the following solution. Good for me.

First, we make everything polymorphic

@JsonTypeResolver(MyTypeResolver.class)
@JsonTypeIdResolver(MyTypeIdResolver.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, property = "@type")
public interface ObjectMixin {

}

ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.addMixIn(Object.class, ObjectMixin.class);

We create a custom TypeResolver, which only handles type serialization/deserialization of java.lang.Object.

public class MyTypeResolver extends StdTypeResolverBuilder {

@Override
    public TypeSerializer buildTypeSerializer(SerializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
        return useForType(baseType) ? super.buildTypeSerializer(config, baseType, subtypes) : null;
    }

@Override
    public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
        return useForType(baseType) ? super.buildTypeDeserializer(config, baseType, subtypes) : null;
    }

public boolean useForType(JavaType t) {
        return t.isJavaLangObject();
    }
}

TypeIdResolver handles ID magic in turn. In this example, everything is hardcoded, and in real code, it certainly looks better. 🙂

public class MyTypeIdResolver extends TypeIdResolverBase {

@Override
    public String idFromValue(Object value) {
        return getId(value);
    }

@Override
    public String idFromValueAndType(Object value, Class<?> suggestedType) {
        return getId(value);
    }

@Override
    public JsonTypeInfo.Id getMechanism() {
        return JsonTypeInfo.Id.CUSTOM;
    }

private String getId(Object value) {
        if (value instanceof ListWrapper.MyMapListWrapper) {
            return "MyMap[]";
        }

if (value instanceof ListWrapper.Child1ListWrapper) {
            return "Child1[]";
        }

if (value instanceof ListWrapper && !( (ListWrapper) value).getValues().isEmpty()) {
            return ((ListWrapper) value).getValues().get(0).getClass().getSimpleName() + "[]";
        }

return value.getClass().getSimpleName();
    }

@Override
    public JavaType typeFromId(DatabindContext context, String id) throws IOException {
        if (id.endsWith("[]")) {
            if (id.startsWith("Child1")) {
                return TypeFactory.defaultInstance().constructParametricType(ListWrapper.class, Child1.class);
            }
            if (id.startsWith("MyMap")) {
                return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), ListWrapper.MyMapListWrapper.class);
            }
        }
        if (id.equals("Child1")) {
            return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), Child1.class);
        }
        if (id.equals("MyMap")) {
            return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), MyMap.class);
        }

return TypeFactory.unknownType();
    }
}

To be able to handle {“@type: “…”, "@values": ...} list, I have a ListWrapper class and subclasses. To do: Reimplement it with custom deserialization logic.

public class ListWrapper<T> {    
    @JsonProperty("@values")
    private List<T> values;

public static class MyMapListWrapper extends ListWrapper<MyMap> {
    }

public static class Child1ListWrapper extends ListWrapper<Child1> {
    }
}

You can skip the creation of subclasses and add type information to each element. The java.lang.* class certainly has no type information.

The model is:

public class Parent {
    private String prop;
    private Child1 child1;
    private MyMap map;
}

public class Child1 {
    private int anInt;
}

Test code:

@Test
public void shouldDoTheTrick() throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
    mapper.addMixIn(Object.class, ObjectMixin.class);

Parent parent = new Parent("Hello", new Child1(-1), new MyMap() {{
        put("JustString", "JustValue");
        put("List_With_All_MyMaps", new ListWrapper.MyMapListWrapper(new ArrayList<MyMap>() {{
            add(new MyMap() {{
                put("Key", "Value");
                put("object", new Child1(2));
            }});
            add(new MyMap() {{
                put("Key", "Value");
            }});
        }}));
        put("List_With_All_Child1", new ListWrapper.Child1ListWrapper(new ArrayList<Child1>() {{
            add(new Child1(41));
            add(new Child1(42));
        }}));
    }});

String valueAsString = mapper.writeValueAsString(parent);

Parent deser = mapper.readValue(valueAsString, Parent.class);
    assertEquals(parent, deser);
}

JSON output:

{
  "prop" : "Hello",
  "child1" : {
    "anInt" : -1
  },
  "map" : {
    "JustString" : "JustValue",
    "List_With_All_MyMaps" : {
      "@type" : "MyMap[]",
      "@values" : [ {
        "Key" : "Value",
        "object" : {
          "@type" : "Child1",
          "anInt" : 2
        }
      }, {
        "Key" : "Value"
      } ]
    },
    "List_With_All_Child1" : {
      "@type" : "Child1[]",
      "@values" : [ {
        "anInt" : 41
      }, {
        "anInt" : 42
      } ]
    }
  }
}

UPD: Real-world implementation example https://github.com/sdl/dxa-web-application-java/commit/7a36a9598ac2273007806285ea4d854db1434ac5

Related Problems and Solutions