Если вы хотите отличить поля значений null
от отсутствующих полей, наиболее общий метод будет использовать Map
или JsonNode
вместо POJO
. POJO
класс имеет постоянную структуру, Map
или JsonNode
имеют динамический - содержит только то, что вы на самом деле положили туда. Давайте создадим простое приложение, которое читает XML
полезную нагрузку из файла и создает JSON
ответ:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./resource/test.xml").getAbsoluteFile();
XmlMapper xmlMapper = new XmlMapper();
Map map = xmlMapper.readValue(xmlFile, Map.class);
ObjectMapper jsonMapper = new ObjectMapper();
String json = jsonMapper.writeValueAsString(map);
System.out.println(json);
}
}
Теперь рассмотрим несколько примеров, в которых мы проверяем, что JSON
будет сгенерировано для empty
, null
и отсутствующих узлов.
Тест 0-0
Ввод XML
:
<Root>
<a>A</a>
<b>1</b>
<c>
<c1>Rick</c1>
<c2>58</c2>
</c>
</Root>
Результат JSON
:
{"a":"A","b":"1","c":{"c1":"Rick","c2":"58"}}
Тест 0-1
Ввод XML
:
<Root>
<a>A</a>
<c>
<c1>Rick</c1>
<c2/>
</c>
</Root>
Вывод JSON
:
{"a":"A","c":{"c1":"Rick","c2":null}}
Тест 0-2
Ввод XML
:
<Root>
<c/>
</Root>
Вывод JSON
:
{"c":null}
Самая большая проблема с этим простым и быстрым решением - мы потеряли информацию о типах для примитивов. Например, если b
равно Integer
, мы должны вернуть его в JSON
как числовой примитив, который не имеет кавычек: "
символ вокруг. Для решения этой проблемы мы должны использовать модель POJO
, которая позволяет нам найти все необходимые типы. Давайте создадим модель POJO
для нашего примера:
@JsonFilter("allowedFields")
class Root {
private String a;
private Integer b;
private C c;
// getters, setters
}
@JsonFilter("allowedFields")
class C {
private String c1;
private Integer c2;
// getters, setters
}
Нам нужно изменить наш простой XML -> Map -> JSON
алгоритм на следующий:
- Читать JSON как
Map
или JsonNode
- Найти все имена полей
- Создать
FilterProvider
с найденными именами - обратите внимание, что фильтр зарегистрирован с именем allowedFields
, то же самое, что используется в аннотации @JsonFilter
.
- Преобразование
Map
в POJO
для приведения типов.
- Запись
POJO
с фильтром
Простое приложение может выглядеть так:
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
public class JsonApp {
public static void main(String[] args) throws Exception {
File xmlFile = new File("./resource/test.xml").getAbsoluteFile();
NodesWalker walker = new NodesWalker();
XmlMapper xmlMapper = new XmlMapper();
JsonNode root = xmlMapper.readValue(xmlFile, JsonNode.class);
Set<String> names = walker.findAllNames(root);
SimpleFilterProvider filterProvider = new SimpleFilterProvider();
filterProvider.addFilter("allowedFields", SimpleBeanPropertyFilter.filterOutAllExcept(names));
ObjectMapper jsonMapper = new ObjectMapper();
jsonMapper.setFilterProvider(filterProvider);
Root rootConverted = jsonMapper.convertValue(root, Root.class);
String json = jsonMapper.writeValueAsString(rootConverted);
System.out.println(json);
}
}
class NodesWalker {
public Set<String> findAllNames(JsonNode node) {
Set<String> names = new HashSet<>();
LinkedList<JsonNode> nodes = new LinkedList<>();
nodes.add(node);
while (nodes.size() > 0) {
JsonNode first = nodes.removeFirst();
if (first.isObject()) {
ObjectNode objectNode = (ObjectNode) first;
objectNode.fields().forEachRemaining(e -> {
names.add(e.getKey());
JsonNode value = e.getValue();
if (value.isObject() || value.isArray()) {
nodes.add(value);
}
});
} else if (first.isArray()) {
ArrayNode arrayNode = (ArrayNode) first;
arrayNode.elements().forEachRemaining(e -> {
if (e.isObject() || e.isArray()) {
nodes.add(e);
}
});
}
}
return names;
}
}
Тест 1-0
Ввод XML
:
<Root>
<a>A</a>
<b>1</b>
<c>
<c1>Rick</c1>
<c2>58</c2>
</c>
</Root>
Вывод JSON
:
{"a":"A","b":1,"c":{"c1":"Rick","c2":58}}
Тест 1-1
Ввод XML
:
<Root>
<b>1</b>
<c>
<c2/>
</c>
</Root>
Вывод JSON
:
{"b":1,"c":{"c2":null}}
Тест 1-2
Ввод XML
:
<Root>
<c/>
</Root>
Вывод JSON
:
{"c":null}
После всех этих тестов мы видим, что динамическая проверка, является ли поле null
, empty
или absent
, не простая задача. Тем не менее, выше 2 решения работают для простых моделей, вы должны проверить их для всех ответов, которые вы хотите сгенерировать. Когда модель сложная и содержит много сложных аннотаций, таких как: @JsonTypeInfo
, @JsonSubTypes
на Jackson
стороне или @XmlElementWrapper
, @XmlAnyElement
на JAXB
стороне, это затрудняет выполнение этой задачи.
Я думаю, что лучшим решением в вашем примере является использование @JsonInclude(NON_NULL)
, который отправляет клиенту все установленные поля на стороне XML
. null
и absent
должны обрабатываться на стороне клиента одинаково. Бизнес-логика не должна полагаться на то, что поле факта установлено в null
или absent
в JSON
полезной нагрузке.
Смотри также: