Как отличить нулевые поля значений от отсутствующих полей в библиотеке Джексона - PullRequest
2 голосов
/ 25 марта 2019

Мы используем API, а API предоставляет поля XML. Мы должны конвертировать XML в JSON для наших потребителей. У нас есть требование показывать только то, что мы получили в виде XML, и отображать только эти поля.

  1. Если поля существуют со значением, указанным в нем
  2. Если поля не существуют, не указывайте их
  3. если поле существует с нулевым / нулевым значением, отобразить поле как есть.

То, что я видел, это общие аннотации

@JsonInclude(NON_EMPTY) можно использовать для исключения пустых значений. Я не могу использовать это, потому что все еще хочу видеть пустые поля со значением NULL в json

@JsonInclude(NON_ABSENT) может использоваться для исключения нулевых значений и значений, которые «отсутствуют». Я не могу использовать это, потому что я все еще хочу видеть пустые поля и нулевые поля в json. То же самое с JsonInclude (NON_NULL)

Итак, мой вопрос: если я не укажу ни одно из этих свойств, могу ли я достичь того, чего хочу? Другими словами, если я не укажу ничего из этого, будет ли поведение Джексона показывать все поля, которые имеют нулевое значение в динамическом смысле? Моя главная проблема - это динамичный ответ здесь. Для каждого запроса поля могут присутствовать или не присутствовать. Мы должны показать в json, что именно мы получаем в XML

1 Ответ

1 голос
/ 26 марта 2019

Если вы хотите отличить поля значений 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 алгоритм на следующий:

  1. Читать JSON как Map или JsonNode
  2. Найти все имена полей
  3. Создать FilterProvider с найденными именами - обратите внимание, что фильтр зарегистрирован с именем allowedFields, то же самое, что используется в аннотации @JsonFilter.
  4. Преобразование Map в POJO для приведения типов.
  5. Запись 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 полезной нагрузке.

Смотри также:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...