Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserializing polymorphic types with Jackson based on the presence of a unique property

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".

like image 617
Sam Berry Avatar asked Jun 17 '14 12:06

Sam Berry


People also ask

What is the use of Jackson ObjectMapper?

ObjectMapper is the main actor class of Jackson library. ObjectMapper class ObjectMapper provides functionality for reading and writing JSON, either to and from basic POJOs (Plain Old Java Objects), or to and from a general-purpose JSON Tree Model (JsonNode), as well as related functionality for performing conversions.

What is Jackson object serialization?

Jackson is a solid and mature JSON serialization/deserialization library for Java. The ObjectMapper API provides a straightforward way to parse and generate JSON response objects with a lot of flexibility.

How does Jackson read nested JSON?

A JsonNode is Jackson's tree model for JSON and it can read JSON into a JsonNode instance and write a JsonNode out to JSON. To read JSON into a JsonNode with Jackson by creating ObjectMapper instance and call the readValue() method. We can access a field, array or nested object using the get() method of JsonNode class.

What is polymorphic JSON?

You can use this schema when defining XML Type hierarchies by using only the base XML Types. The XML schema defines XML Types that inherit from each other. In the JSON, an object carries no additional information about the type.


2 Answers

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

like image 186
bernie Avatar answered Sep 28 '22 00:09

bernie


This feels like something @JsonTypeInfo and @JsonSubTypes should be used for but I've picked through the docs and none of the properties that can be supplied quite seem to match what you're describing.

You could write a custom deserializer that uses @JsonSubTypes' "name" and "value" properties in a non-standard way to accomplish what you want. The deserializer and @JsonSubTypes would be supplied on your base class and the deserializer would use the "name" values to check for the presence of a property and if it exists, then deserialize the JSON into the class supplied in the "value" property. Your classes would then look something like this:

@JsonDeserialize(using = PropertyPresentDeserializer.class) @JsonSubTypes({         @Type(name = "stringA", value = SubClassA.class),         @Type(name = "stringB", value = SubClassB.class) }) 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;     ... } 
like image 38
Erik Gillespie Avatar answered Sep 28 '22 02:09

Erik Gillespie