У меня есть следующий класс
public class Customer<T>{
private String name;
private String store;
private T details;
}
public class Details{
private String dob;
private boolean validated;
}
Десериализация работает, когда JSON имеет следующий вид
{
"name": "John Smith",
"store": "Walmart",
"details": {
"dob": "1900/01/01",
"validated": true
}
}
Но когда данный ответ JSON похож на приведенный ниже, он терпит неудачу.
{
"name": "John Smith",
"store": "Walmart",
"details": [
"Failed customer does not exist in the system", "Please contact administrator"
]
}
Есть ли способ с помощью картографа Джексона справиться с этими сценариями? Если details
предоставляется как POJO, десериализуйте его как таковой, в противном случае десериализуйте details
как String
. Я определил свой картограф следующим образом:
private static final ObjectMapper Mapper = new ObjectMapper() {{
setSerializationInclusion(JsonInclude.Include.NON_NULL);
setDateFormat(new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z", Locale.US));
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}};
Кроме того, каков ожидаемый результат, когда details
появляется в виде массива строк в json, объединяя строки в одну и присваивая ее свойству dob
объекта Details
?
Я использую общий тип T для проверки в будущем, если вместо Details
получу другой объект. Моим ожидаемым результатом для получения подробной информации будет сохранение массива json в виде одной строки в поле.
Если до десериализации вы уже знали, что содержит json для поля details
, вы могли бы просто прочитать общий Customer
с помощью TypeReference
. Однако в этом сценарии сообщения об ошибках будут иметь вид List<String>
вместо String
, больше придерживаясь структуры json.
//Reading a generic Customer with a Details pojo
Customer<Details> c1 = mapper.readValue(jsonPojo, new TypeReference<Customer<Details>>() {});
//Reading a generic Customer with an array of String error messages
Customer<List<String>> c2 = mapper.readValue(jsonArray, new TypeReference<Customer<List<String>>>() {});
Однако это решение требует заранее знать, что содержит json (чего не может быть), или заранее проанализировать его, а затем приступить к соответствующей десериализации (что может быть утомительным).
Итак, чтобы предложить более общее решение, сохраняя при этом гибкость дженериков, необходимо использовать собственный десериализатор для поля details
, поскольку логика весьма специфична и конфигурации только на ObjectMapper
этого не сделают.
Если сведения указаны как pojo, то запишите их как таковые, в противном случае запишите сведения как строковое значение.
Однако определение пользовательского десериализатора для универсального типа T
не имеет особого смысла, поскольку после реализации логики десериализации для двух случаев Details
и String
мы будем вынуждены привести результат к универсальному типу T
, что сводит на нет весь смысл использования дженериков. и теряют свое главное преимущество безопасности типов во время компиляции.
Вместо этого мы могли бы найти общий знаменатель типов среди возможных типов данных для поля details
. На данный момент details
может быть либо экземпляром Details
, либо String
. Класс String
реализует интерфейс Serializable
, поэтому, если бы класс Details
тоже реализовал его, мы могли бы использовать Serializable
в качестве верхней границы для универсального типа T
и определить собственный десериализатор для Serializable
. Этот подход подразумевает, что каждый тип, передаваемый вместо T
, должен быть подтипом Serializable
. Это не очень строгое требование, поскольку большинство классов Java уже реализуют интерфейс, в то время как наши классы также могут легко реализовать его без необходимости предоставления какой-либо реализации.
public class Customer<T extends Serializable> {
private String name;
private String store;
@JsonDeserialize(using = MyDeserializer.class)
private T details;
// ... implementation ...
}
public class Details implements Serializable {
private String dob;
private boolean validated;
// ... implementation ...
}
public class MyDeserializer extends StdDeserializer<Serializable> {
public MyDeserializer() {
this(null);
}
public MyDeserializer(Class<?> c) {
super(c);
}
@Override
public Serializable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
if (node.isArray()) {
return StreamSupport.<JsonNode>stream(Spliterators.spliteratorUnknownSize(node.iterator(), Spliterator.ORDERED), false)
.map(JsonNode::textValue)
.collect(Collectors.joining(" - "));
}
return ctxt.readTreeAsValue(node, Details.class);
}
}
public class Main {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper() {{
setSerializationInclusion(JsonInclude.Include.NON_NULL);
setDateFormat(new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z", Locale.US));
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}};
String jsonPojo = """
{
"name": "John Smith",
"store": "Walmart",
"details": {
"dob": "1900/01/01",
"validated": true
}
}
""";
Customer<Serializable> c1 = mapper.readValue(jsonPojo, new TypeReference<Customer<Serializable>>() {});
System.out.println(c1.getDetails().getClass().getSimpleName()); //Prints Details
System.out.println(c1);
String jsonArray = """
{
"name": "John Smith",
"store": "Walmart",
"details": [
"Failed customer does not exist in the system", "Please contact administrator"
]
}
""";
Customer<Serializable> c2 = mapper.readValue(jsonArray, new TypeReference<Customer<Serializable>>() {});
System.out.println(c2.getDetails().getClass().getSimpleName()); //Prints String
System.out.println(c2);
}
}
Вот также демо-версия на OneCompiler, демонстрирующая приведенный выше код.
Это идеальное решение с подробным и полезным объяснением. Спасибо, что изменили вопрос, чтобы ваш ответ был более ясным.
@greatFritz Спасибо! Я только сейчас понял, что копируя код, я перепутал решение с другой попыткой. Класс MyDeserializer
возвращал экземпляр Details
в обоих случаях, тогда как основная часть теста не использовала TypeReference<Serializable>
для чтения строк json. Я также обновил демо-версию.
Вы использовали общий тип
T
дляdetails
в классеCustomer
, но в объявлении класса нет раздела параметров типа. Вы имели в видуprivate Details details
вместоprivate T details
?