Настроить XStream для динамического сопоставления с различными объектами

Я использую RESTful сторонний API, который всегда отправляет JSON в следующем формате:

{
    "response": {
        ...
    }
}

куда ... является объектом ответа, который должен быть отображен обратно в Java POJO. Например, иногда JSON будет содержать данные, которые должны быть сопоставлены с Fruit POJO:

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

... а иногда JSON будет содержать данные, которые должны быть сопоставлены с Employee POJO:

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

Таким образом, в зависимости от вызова API RESTful нам необходимо сопоставить эти два результата JSON с одним из двух:

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
}

К сожалению, я не могу изменить тот факт, что этот сторонний REST-сервис всегда отправляет обратно { "response": { ... } } Результат в формате JSON. Но мне все еще нужен способ настроить маппер для динамического отображения такого response вернуться к любому Fruit или Employee,

Во-первых, я попробовал Джексона с ограниченным успехом, но это было не так настраиваемо, как хотелось бы. Так что теперь я пытаюсь использовать XStream с его JettisonMappedXmlDriver для отображения JSON обратно в POJO. Вот код прототипа, который у меня есть:

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);
}

К сожалению, когда я запускаю это, я получаю исключение, потому что у меня есть xs.alias("response", ...) отображение response на 2 разных Java-объекта:

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

Поэтому я спрашиваю: что я могу сделать, чтобы обойти тот факт, что API всегда будет отправлять обратно одну и ту же "оболочку" response Объект JSON? Единственное, о чем я могу подумать, это сначала выполнить String-replace следующим образом:

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

Но это похоже на уродливый хак. Предоставляет ли XStream (или другая структура отображения) что-нибудь, что могло бы помочь мне в этом конкретном случае? Заранее спасибо.

2 ответа

Решение

Есть два пути с Джексоном:

  • проверить вручную, что нужные ключи есть (JsonNode имеет необходимые методы);
  • использовать схему JSON; в Java есть один API: https://github.com/fge/json-schema-validator (да, это мое), который использует Джексона.

Напишите схему, соответствующую вашему первому типу объекта:

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

Загрузите это как схему, проверьте свои входные данные по ней: если это проверяет, вы знаете, что вам нужно десериализовать по вашему классу фруктов. В противном случае создайте схему для второго типа элемента, проверьте ее в качестве меры безопасности и десериализуйте, используя другой класс.

Также есть примеры кода для API(версия 1.4.x)

Если вы знаете настоящий тип, он должен быть относительно простым с Джексоном. Вам нужно использовать универсальный тип оболочки, такой как:

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

и тогда единственный трюк состоит в том, чтобы создать объект типа, чтобы Джексон знал, что T есть. Если он статически доступен, вы просто делаете:

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

но если это генерируется более динамически, что-то вроде:

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;

но в любом случае это "должно просто работать". Обратите внимание, что в последнем случае вы можете использовать базовые типы ("? Extends MyBaseType"), но в целом динамический тип не может быть указан.

Другие вопросы по тегам