Deserializing polymorphic types with Jackson

Sam Berry picture Sam Berry · Jun 17, 2014 · Viewed 25.7k times · Source

If I have a class structure like so:

public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}

Is there an alternative way to deserialize different then @JsonTypeInfo? Using this annotation on my parent class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "objectType")

I would rather not have to force clients of my API to include "objectType": "SubClassA" to deserialize a Parent subclass.

Instead of using @JsonTypeInfo, does Jackson provide a way to annotate a subclass and distinguish it from other subclasses via a unique property? In my example above, this would be something like, "If a JSON object has "stringA": ... deserialize it as SubClassA, if it has "stringB": ... deserialize it as SubClassB".

Answer

bernie picture bernie · Apr 25, 2018

Here's a solution I've come up with that expands a bit on Erik Gillespie's. It does exactly what you asked for and it worked for me.

Using Jackson 2.9

@JsonDeserialize(using = CustomDeserializer.class)
public abstract class BaseClass {

    private String commonProp;
}

// Important to override the base class' usage of CustomDeserializer which produces an infinite loop
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassA extends BaseClass {
    
    private String classAProp;
}

@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassB extends BaseClass {
    
    private String classBProp;
}

public class CustomDeserializer extends StdDeserializer<BaseClass> {

    protected CustomDeserializer() {
        super(BaseClass.class);
    }

    @Override
    public BaseClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        TreeNode node = p.readValueAsTree();
        
        // Select the concrete class based on the existence of a property
        if (node.get("classAProp") != null) {
            return p.getCodec().treeToValue(node, ClassA.class);
        }
        return p.getCodec().treeToValue(node, ClassB.class);
    }
}

// Example usage
String json = ...
ObjectMapper mapper = ...
BaseClass instance = mapper.readValue(json, BaseClass.class);

If you want to get fancier, you can expand CustomDeserializer to include a Map<String, Class<?>> that maps a property name that, when present, maps to a specific class. Such an approach is presented in this article.

Update

Jackson 2.12.0 gets Polymorphic subtype deduction from available fields which adds @JsonTypeInfo(use = DEDUCTION)!

AsDeductionTypeDeserializer implements inferential deduction of a subtype from the fields. As a POC not intended for merging, tere's an amount of cut'n'paste code etc but I thought a functional PR would be the best basis for discussion of something I write out of interest.

It works by fingerprinting the full set of possible fields of each subtype on registration. On deserialisation, available fields are compared to those fingerprints until only one candidate remains. It specifically only looks at immediate-child field names as is immediate-child values are covered by existing mechanisms and deeper analysis is a much more imposing ML task not really part of Jackson's remit.

By the way, there's a (now closed) Github issue requesting this here: https://github.com/FasterXML/jackson-databind/issues/1627