javajsonjacksonxstreamoxm

Configure XStream to dynamically map to different objects


I am hitting a RESTful 3rd party API that always sends JSON in the following format:

{
    "response": {
        ...
    }
}

Where ... is the response object that needs to be mapped back to a Java POJO. For instance, sometimes the JSON will contain data that should be mapped back to a Fruit POJO:

{
    "response": {
        "type": "orange",
        "shape": "round"
    }
}

...and sometimes the JSON will contain data that should be mapped back to an Employee POJO:

{
    "response": {
        "name": "John Smith",
        "employee_ID": "12345",
        "isSupervisor": "true",
        "jobTitle": "Chief Burninator"
    }
}

So depending on the RESTful API call, we need these two JSON results mapped back to one of the two:

public class Fruit {
    private String type;
    private String shape;

    // Getters & setters for all properties
}

public class Employee {
    private String name;
    private Integer employeeId;
    private Boolean isSupervisor;
    private String jobTitle;

    // Getters & setters for all properties
}

Unfortunately, I cannot change the fact that this 3rd party REST service always sends back a { "response": { ... } } JSON result. But I still need a way to configure a mapper to dynamically map such a response back to either a Fruit or an Employee.

First, I tried Jackson with limited success, but it wasn't as configurable as I wanted it to be. So now I am trying to use XStream with its JettisonMappedXmlDriver for mapping JSON back to POJOs. Here's the prototype code I have:

public static void main(String[] args) {
    XStream xs = new XStream(new JettisonMappedXmlDriver());

    xs.alias("response", Fruit.class);
    xs.alias("response", Employee.class);

    // When XStream sees "employee_ID" in the JSON, replace it with
    // "employeeID" to match the field on the POJO.
    xs.aliasField("employeeID", Employee.class, "employee_ID");

    // Hits 3rd party RESTful API and returns the "*fruit version*" of the JSON.
    String json = externalService.getFruit();

    Fruit fruit = (Fruit)xs.fromXML(json);
}

Unfortunately when I run this I get an exception, because I have xs.alias("response", ...) mapping response to 2 different Java objects:

Caused by: com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter$UnknownFieldException: No such field me.myorg.myapp.domain.Employee.type
---- Debugging information ----
field               : type
class               : me.myorg.myapp.domain.Employee
required-type       : me.myorg.myapp.domain.Employee
converter-type      : com.thoughtworks.xstream.converters.reflection.ReflectionConverter
path                : /response/type
line number         : -1
version             : null
-------------------------------

So I ask: what can I do to circumvent the fact that the API will always send back the same "wrapper" response JSON object? The only thing I can think of is first doing a String-replace like so:

String json = externalService.getFruit();
json = json.replaceAll("response", "fruit");
...

But this seems like an ugly hack. Does XStream (or another mapping framework) provide anything that would help me out in this particular case? Thansk in advance.


Solution

  • There are two ways with Jackson:

    Write a schema matching your first object type:

    {
        "type": "object",
        "properties": {
            "type": {
                "type": "string",
                "required": true
            },
            "shape": {
                "type": "string",
                "required": true
            }
        },
        "additionalProperties": false
    }
    

    Load this as a schema, validate your input against it: if it validates, you know you need to deserialize against your fruit class. Otherwise, make the schema for the second item type, validate against it as a security measure, and deserialize using the other class.

    There are code examples for the API, too (version 1.4.x)