Search code examples
javajsonjacksondeserializationjson-deserialization

Polymorphic Deserialization Issue with Jackson


I have the following classes that I want to deserialize a JSON string to using Jackson.

PushNotificationMessage.java

public class PushNotificationMessage {
    @JsonProperty("device_info")
    private DeviceInfo deviceInfo;
    private String content;

    //getters & setters
}

DeviceInfo.java

public class DeviceInfo {

    @JsonProperty(value = "device_type")
    private String deviceType;

    //getters & setters
}

IOSDeviceInfo.java

public class IOSDeviceInfo extends DeviceInfo {

    @JsonProperty(value = "device_id")
    private String deviceId;
    private String arn;
    @JsonProperty(value = "user_data")
    private String userData;

    //getters & setters
}

WebDeviceInfo.java

public class WebDeviceInfo extends DeviceInfo {
    private String endpoint;
    private String key;
    private String auth;

    //getters & setters
}

I have the following JSON content that I want to deserialize:

{
    "device_info": {
        "endpoint": "https://android.googleapis.com/gcm/send/blah",
        "key": "blahkey",
        "auth": "blahauth",
        "device_type": "web"
    },
    "content": "Notification content"
}

I simply use ObjectMapper to try to perform the deserialization as such.

final ObjectMapper objectMapper = new ObjectMapper();
final PushNotificationMessage message = objectMapper.readValue(jsonString, PushNotifictionMessage.class);

When I do this I get:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "endpoint" (class com.blah.DeviceInfo), not marked as ignorable (one known property: "device_type"])

How can I get Jackson to recognize that it needs to be mapped to a WebDeviceInfo instance, instead of trying to map it to the DeviceInfo superclass, which does not have the endpoint field?

I've tried playing with @JsonTypeInfo and @JsonSubTypes annotations in my different classes, but I can find no good examples of how to use them.

EDIT: I added the @JsonDeserialize(using = DeviceInfoDeserializer.class) annotation to my DeviceInfo class, and created the following DeviceInfoDeserializer.

public class DeviceInfoDeserializer extends JsonDeserializer<DeviceInfo> {

    private static final String DEVICE_TYPE = "device_type";
    private static final String WEB = "web";
    private static final String IOS = "ios";

    @Override
    public DeviceInfo deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
        final ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec();
        final ObjectNode root = objectMapper.readTree(jsonParser);
        if (root.has(DEVICE_TYPE)) {
            final JsonNode jsonNode = root.get(DEVICE_TYPE);
            if (jsonNode.asText().equalsIgnoreCase(WEB)) {
                return objectMapper.readValue(root.toString(), WebDeviceInfo.class);
            } else if (jsonNode.asText().equalsIgnoreCase(IOS)) {
                return objectMapper.readValue(root.toString(), IOSDeviceInfo.class);
            }
        }
        throw deserializationContext.mappingException("Failed to de-serialize device info, as device_type was not \"web\" or \"ios\"");
    }
}

Now, I get a different error when attempting to deserialize my PushNotificationMessage JSON:

java.lang.StackOverflowError: null
    at com.fasterxml.jackson.databind.deser.std.BaseNodeDeserializer.deserializeObject(JsonNodeDeserializer.java:210)
    at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:69)
    at com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer.deserialize(JsonNodeDeserializer.java:15)
    at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3770)
    at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2207)
    at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:25)
    at com.blah.serialization.DeviceInfoDeserializer.deserialize(DeviceInfoDeserializer.java:16)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
    ... (above trace repeated many times)

EDIT: Just needed to add @JsonDeserialize(as = WebDeviceInfo.class) and @JsonDeserialize(as = IOSDeviceInfo.class) to my subclasses, now it works as expected. Big thank you to @Luciano van der Veekens.


Solution

  • Jackson is not aware of polymorphism, it just tries to create an instance of the concrete DeviceInfo class.

    However, you can implement a custom deserializer that programmatically parses the device info JSON and knows when to instantiate one of the subclasses due to the uniqueness of some fields such as endpoint.

    https://fasterxml.github.io/jackson-databind/javadoc/2.2.0/com/fasterxml/jackson/databind/annotation/JsonDeserialize.html

    @JsonDeserialize(using = DeviceInfoDeserializer.class)
    public class DeviceInfo {
    }
    

    An example can be found here: http://sunilkumarpblog.blogspot.nl/2015/12/javajson-polymorphic-serialization-de.html