Frage

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.

War es hilfreich?

Lösung

There are two ways with Jackson:

  • test manually that the wanted keys are there (JsonNode has the necessary methods);
  • use JSON Schema; there is one API in Java: json-schema-validator (yes, that is mine), which uses 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)

Andere Tipps

If you do know the actual type, it should be relatively straight-forward with Jackson. You need to use a generic wrapper type like:

public class Wrapper<T> {
  public T response;
}

and then the only trick is to construct type object to let Jackson know what T there is. If it is statically available, you just do:

Wrapper<Fruit> wrapped = mapper.readValue(input, new TypeReference<Wrapper<Fruit>>() { });
Fruit fruit = wrapped.response;

but if it is more dynamically generated, something like:

Class<?> rawType = ... ; // determined using whatever logic is needed
JavaType actualType = mapper.getTypeFactory().constructGenericType(Wrapper.class, rawType);
Wrapper<?> wrapper = mapper.readValue(input, actualType);
Object value = wrapper.response;

but either way it "should just work". Note that in latter case you may be able to use base types ("? extends MyBaseType"), but in general dynamic type can't be specified.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top