Настроить 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"), но в целом динамический тип не может быть указан.