0

I want to use Jackson to dynamically deserialize objects into the appropriate java classes, but I'm having trouble configuring Jackson the right way.

I have the following simplified model (getters/setters omitted for brevity):

class GeneralObject {
    public String objType;
    public String commonProp;
    public GeneralObject nestedObject;

    // map for additional properties, so that I can re-serialize the full object later
    public Map<String, JsonNode> additionalFields = new HashMap<>();
    @JsonAnyGetter
    public Map<String, JsonNode> getAdditionalFields() {
        return additionalFields;
    }
    @JsonAnySetter
    public void addAdditionalField(String fieldName, JsonNode value) {
        this.additionalFields.put(fieldName, value);
    }
}

class SpecialObject extends GeneralObject {
    public String specialProp;
}

In reality, there are different kinds of "special Objects", and I want to be able to add more in the future, when the need comes.

The jsons look like this (I get them from external sources, I cannot change the format in which they are sent):

{
  "objType": "someType1",
  "commonProp": "example1..."
}
{
  "objType": "SPECIAL",
  "commonProp": "example2...",
  "specialProp": "more example"
}
{
  "objType": "someOtherType",
  "commonProp": "example3...",
  "nestedObject": {
    "objType": "SPECIAL",
    "commonProp": "example2...",
    "specialProp": "more example"
  }
}

I am currently parsing them like this:

ObjectMapper mapper = new ObjectMapper();
String objString = "{\"objType\": \"SPECIAL\", \"commonProp\": \"...\", \"specialProp\": \"more example\"}";
GeneralObject genObj = mapper.readValue(objString, GeneralObject.class);
if (genObj.objType.equals("SPECIAL")) {
    genObj = mapper.readValue(objString, SpecialObject.class);
}
// Some business-logic: If SPECIAL, then this cast is required to work:
System.out.println(((SpecialObject) genObj).specialProp);

This works for the top-level object, but not for the nested objects. If, for example, the nested Object is a SPECIAL object, it will still be deserialized as a common object.

What I want do do is to tell Jackson: "No matter the nesting-level, if objType=SPECIAL, use SpecialObject, else use GeneralObject". I looked into Polymorphic Deserialization and tried using @JsonSubTypes, but could not set up this logic correctly. How can I make sure that SPECIAL objects are deserialized into the appropriate class, even if they are nested?

Martin J.H.
  • 2,085
  • 1
  • 22
  • 37

1 Answers1

1

I first took inspiration from this Gist and tried to solved it with a custom TypeIdResolver. Unfortunately, this had the problem of not deserializing the objType properly (see first version of this answer).

I then got inspiration from this answer and switched to a custom Deserializer:

class CustomDeserializer extends StdDeserializer<GeneralObject> {

    private static final String SPECIAL = "\"SPECIAL\"";

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

    @Override
    public GeneralObject deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
        TreeNode node = p.readValueAsTree();

        // Select appropriate class based on "resourceType"
        TreeNode objTypeNode = node.get("objType");
        if (null == objTypeNode) {
            throw new JsonParseException(p, "field \"objType\" is missing!");
        }
        if (!objTypeNode.isValueNode()) {
            throw new JsonParseException(p, "field \"objType\" must be a String.");
        }
        String objType = objTypeNode.toString();
        Class<? extends GeneralObject> clazz;
        if (objType.equals(SPECIAL)) {
            clazz = SpecialObject.class;
        } else {
            clazz = RecursionStopper.class;
        }
        return p.getCodec().treeToValue(node, clazz);
    }
}

It checks the contents of .objType and emits the appropriate (sub)-class that should be used for deserialization. The deserializer needs to be registered on the GeneralObject, for example by using the following annotation:

@JsonDeserialize(using = CustomDeserializer.class)
class GeneralObject {
    ...
}

To stop an infinite recursion loop from occurring, all sub-classes must be annotated not to use this custom deserializer, and we need to introduce a helper class that stops the recursion for the GeneralObject:

@JsonDeserialize(using = JsonDeserializer.None.class)
class SpecialObject extends GeneralObject {
    public String specialProp;
}

@JsonDeserialize(using = JsonDeserializer.None.class)
class RecursionStopper extends GeneralObject {
    // this class intentionally empty
}

The deserialization works as intended, also for nested objects:

ObjectMapper mapper = new ObjectMapper();
String objString = "{\n" +
        "  \"objType\": \"someObjType\",\n" +
        "  \"commonProp\": \"example3...\",\n" +
        "  \"nestedObject\": {\n" +
        "    \"objType\": \"SPECIAL\",\n" +
        "    \"commonProp\": \"example2...\",\n" +
        "    \"specialProp\": \"more example\"\n" +
        "  }\n" +
        "}";
GeneralObject genObj = mapper.readValue(objString, GeneralObject.class);
System.out.println(((SpecialObject) genObj.nestedObject).specialProp);
Martin J.H.
  • 2,085
  • 1
  • 22
  • 37