Перейти к основному содержимому

Работа с узлами модели дерева в Джексоне

· 9 мин. чтения

1. Обзор

В этом руководстве основное внимание будет уделено работе с узлами модели дерева в Jackson.

Мы будем использовать JsonNode для различных преобразований, а также для добавления, изменения и удаления узлов.

2. Создание узла

Первым шагом в создании узла является создание экземпляра объекта ObjectMapper с помощью конструктора по умолчанию:

ObjectMapper mapper = new ObjectMapper();

Поскольку создание объекта ObjectMapper обходится дорого, рекомендуется повторно использовать один и тот же объект для нескольких операций.

Далее у нас есть три разных способа создания узла дерева, когда у нас есть ObjectMapper .

2.1. Создайте узел с нуля

Это самый распространенный способ создать узел из ничего:

JsonNode node = mapper.createObjectNode();

Кроме того, мы также можем создать узел через JsonNodeFactory :

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Разбор из источника JSON

Этот метод хорошо описан в статье Jackson — Marshall String to JsonNode . Пожалуйста, обратитесь к нему для получения дополнительной информации.

2.3. Преобразование из объекта

Узел можно преобразовать из объекта Java, вызвав метод valueToTree(Object fromValue) в ObjectMapper :

JsonNode node = mapper.valueToTree(fromValue);

Здесь также полезен convertValue API:

JsonNode node = mapper.convertValue(fromValue, JsonNode.class);

Давайте посмотрим, как это работает на практике.

Предположим, у нас есть класс с именем NodeBean :

public class NodeBean {
private int id;
private String name;

public NodeBean() {
}

public NodeBean(int id, String name) {
this.id = id;
this.name = name;
}

// standard getters and setters
}

Давайте напишем тест, который удостоверится, что преобразование происходит правильно:

@Test
public void givenAnObject_whenConvertingIntoNode_thenCorrect() {
NodeBean fromValue = new NodeBean(2016, "foreach.com");

JsonNode node = mapper.valueToTree(fromValue);

assertEquals(2016, node.get("id").intValue());
assertEquals("foreach.com", node.get("name").textValue());
}

3. Преобразование узла

3.1. Записать как JSON

Это основной метод преобразования узла дерева в строку JSON, где назначением может быть File , OutputStream или Writer :

mapper.writeValue(destination, node);

Повторно используя класс NodeBean, объявленный в разделе 2.3, тест гарантирует, что этот метод работает должным образом:

final String pathToTestFile = "node_to_json_test.json";

@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
String newString = "{\"nick\": \"cowtowncoder\"}";
JsonNode newNode = mapper.readTree(newString);

JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).set("name", newNode);

assertFalse(rootNode.path("name").path("nick").isMissingNode());
assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}

3.2. Преобразовать в объект

Самый удобный способ преобразовать JsonNode в объект Java — это API treeToValue :

NodeBean toValue = mapper.treeToValue(node, NodeBean.class);

Это функционально эквивалентно следующему:

NodeBean toValue = mapper.convertValue(node, NodeBean.class)

Мы также можем сделать это через поток токенов:

JsonParser parser = mapper.treeAsTokens(node);
NodeBean toValue = mapper.readValue(parser, NodeBean.class);

Наконец, давайте реализуем тест, который проверяет процесс преобразования:

@Test
public void givenANode_whenConvertingIntoAnObject_thenCorrect()
throws JsonProcessingException {
JsonNode node = mapper.createObjectNode();
((ObjectNode) node).put("id", 2016);
((ObjectNode) node).put("name", "foreach.com");

NodeBean toValue = mapper.treeToValue(node, NodeBean.class);

assertEquals(2016, toValue.getId());
assertEquals("foreach.com", toValue.getName());
}

4. Управление узлами дерева

Мы будем использовать следующие элементы JSON, содержащиеся в файле с именем example.json , в качестве базовой структуры для действий, которые необходимо выполнить:

{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},

"title": "Jackson founder",
"company": "FasterXML"
}

Этот файл JSON, расположенный в пути к классам, преобразуется в дерево модели:

public class ExampleStructure {
private static ObjectMapper mapper = new ObjectMapper();

static JsonNode getExampleRoot() throws IOException {
InputStream exampleInput =
ExampleStructure.class.getClassLoader()
.getResourceAsStream("example.json");

JsonNode rootNode = mapper.readTree(exampleInput);
return rootNode;
}
}

Обратите внимание, что корень дерева будет использоваться при иллюстрации операций над узлами в следующих подразделах.

4.1. Расположение узла

Прежде чем работать с любым узлом, первое, что нам нужно сделать, это найти его и присвоить переменной.

Если мы заранее знаем путь к узлу, это довольно легко сделать.

Скажем, нам нужен узел с именем last , который находится под именем node:

JsonNode locatedNode = rootNode.path("name").path("last");

Кроме того, вместо path можно использовать API -интерфейсы get или with . ``

Если путь неизвестен, поиск, конечно, станет более сложным и итеративным.

Мы можем увидеть пример перебора всех узлов в Разделе 5 — Перебор узлов .

4.2. Добавление нового узла

Узел может быть добавлен как дочерний элемент другого узла:

ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);

Многие перегруженные варианты put могут использоваться для добавления новых узлов с разными типами значений.

Также доступны многие другие подобные методы, в том числе putArray , putObject , PutPOJO , putRawValue и putNull .

Наконец, давайте посмотрим на пример, где мы добавляем всю структуру в корневой узел дерева:

"address":
{
"city": "Seattle",
"state": "Washington",
"country": "United States"
}

Вот полный тест, выполняющий все эти операции и проверяющий результаты:

@Test
public void givenANode_whenAddingIntoATree_thenCorrect() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
ObjectNode addedNode = ((ObjectNode) rootNode).putObject("address");
addedNode
.put("city", "Seattle")
.put("state", "Washington")
.put("country", "United States");

assertFalse(rootNode.path("address").isMissingNode());

assertEquals("Seattle", rootNode.path("address").path("city").textValue());
assertEquals("Washington", rootNode.path("address").path("state").textValue());
assertEquals(
"United States", rootNode.path("address").path("country").textValue();
}

4.3. Редактирование узла

Экземпляр ObjectNode можно изменить, вызвав метод set(String fieldName, JsonNode value) :

JsonNode locatedNode = locatedNode.set(fieldName, value);

Подобных результатов можно добиться, используя методы replace или setAll для объектов того же типа.

Чтобы убедиться, что метод работает должным образом, мы изменим значение имени поля в корневом узле с объекта first и last на другое, состоящее только из поля nick в тесте:

@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
String newString = "{\"nick\": \"cowtowncoder\"}";
JsonNode newNode = mapper.readTree(newString);

JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).set("name", newNode);

assertFalse(rootNode.path("name").path("nick").isMissingNode());
assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}

4.4. Удаление узла

Узел можно удалить, вызвав API-интерфейс remove(String fieldName) на его родительском узле:

JsonNode removedNode = locatedNode.remove(fieldName);

Чтобы удалить сразу несколько узлов, мы можем вызвать перегруженный метод с параметром типа Collection<String> , который возвращает родительский узел вместо удаляемого:

ObjectNode locatedNode = locatedNode.remove(fieldNames);

В крайнем случае, когда мы хотим удалить все подузлы данного узла, `API removeAll` пригодится.

Следующий тест будет посвящен первому методу, упомянутому выше, который является наиболее распространенным сценарием:

@Test
public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).remove("company");

assertTrue(rootNode.path("company").isMissingNode());
}

5. Перебор узлов

Давайте переберем все узлы в документе JSON и переформатируем их в YAML.

JSON имеет три типа узлов: значение, объект и массив.

Итак, давайте удостоверимся, что наш пример данных имеет все три разных типа, добавив Array :

{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},

"title": "Jackson founder",
"company": "FasterXML",
"pets" : [
{
"type": "dog",
"number": 1
},
{
"type": "fish",
"number": 50
}
]
}

Теперь давайте посмотрим на YAML, который мы хотим создать:

name: 
first: Tatu
last: Saloranta
title: Jackson founder
company: FasterXML
pets:
- type: dog
number: 1
- type: fish
number: 50

Мы знаем, что узлы JSON имеют иерархическую древовидную структуру. Итак, самый простой способ перебрать весь документ JSON — начать сверху и двигаться вниз по всем дочерним узлам.

Мы передадим корневой узел в рекурсивный метод. Затем метод будет вызывать себя с каждым дочерним элементом предоставленного узла.

5.1. Тестирование итерации

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

Наш тест передает корневой узел документа JSON нашему методу toYaml и утверждает, что возвращаемое значение соответствует нашим ожиданиям:

@Test
public void givenANodeTree_whenIteratingSubNodes_thenWeFindExpected() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();

String yaml = onTest.toYaml(rootNode);

assertEquals(expectedYaml, yaml);
}

public String toYaml(JsonNode root) {
StringBuilder yaml = new StringBuilder();
processNode(root, yaml, 0);
return yaml.toString(); }
}

5.2. Работа с различными типами узлов

Нам нужно обрабатывать разные типы узлов немного по-разному.

Мы сделаем это в нашем методе processNode :

private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {
  if (jsonNode.isValueNode()) {
yaml.append(jsonNode.asText());
}
else if (jsonNode.isArray()) {
for (JsonNode arrayItem : jsonNode) {
appendNodeToYaml(arrayItem, yaml, depth, true);
}
}
else if (jsonNode.isObject()) {
appendNodeToYaml(jsonNode, yaml, depth, false);
}
}

Во-первых, давайте рассмотрим узел Value. Мы просто вызываем метод узла asText , чтобы получить строковое представление значения.

Далее, давайте посмотрим на узел Array. Каждый элемент в узле Array сам по себе является JsonNode , поэтому мы перебираем массив и передаем каждый узел методу appendNodeToYaml . Нам также нужно знать, что эти узлы являются частью массива.

К сожалению, сам узел не содержит ничего, что говорило бы нам об этом, поэтому мы передаем флаг в наш метод appendNodeToYaml .

Наконец, мы хотим перебрать все дочерние узлы каждого узла объекта. Один из вариантов — использовать JsonNode.elements .

Однако мы не можем определить имя поля из элемента, потому что оно просто содержит значение поля:

Object  {"first": "Tatu", "last": "Saloranta"}
Value  "Jackson Founder"
Value  "FasterXML"
Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]

Вместо этого мы будем использовать JsonNode.fields, потому что это дает нам доступ как к имени поля, так и к значению:

Key="name", Value=Object  {"first": "Tatu", "last": "Saloranta"}
Key="title", Value=Value  "Jackson Founder"
Key="company", Value=Value  "FasterXML"
Key="pets", Value=Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]

Для каждого поля мы добавляем имя поля к выходным данным, а затем обрабатываем значение как дочерний узел, передавая его методу processNode :

private void appendNodeToYaml(
JsonNode node, StringBuilder yaml, int depth, boolean isArrayItem) {
Iterator<Entry<String, JsonNode>> fields = node.fields();
boolean isFirst = true;
while (fields.hasNext()) {
Entry<String, JsonNode> jsonField = fields.next();
addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
processNode(jsonField.getValue(), yaml, depth+1);
isFirst = false;
}

}

Мы не можем сказать по узлу, сколько у него предков.

Итак, мы передаем поле с именем depth в метод processNode , чтобы отслеживать это, и увеличиваем это значение каждый раз, когда получаем дочерний узел, чтобы мы могли правильно устанавливать отступы полей в нашем выводе YAML:

private void addFieldNameToYaml(
StringBuilder yaml, String fieldName, int depth, boolean isFirstInArray) {
if (yaml.length()>0) {
yaml.append("\n");
int requiredDepth = (isFirstInArray) ? depth-1 : depth;
for(int i = 0; i < requiredDepth; i++) {
yaml.append(" ");
}
if (isFirstInArray) {
yaml.append("- ");
}
}
yaml.append(fieldName);
yaml.append(": ");
}

Теперь, когда у нас есть весь код для перебора узлов и создания вывода YAML, мы можем запустить наш тест, чтобы показать, что он работает.

6. Заключение

В этой статье были рассмотрены распространенные API и сценарии при работе с древовидной моделью в Jackson.

И, как всегда, реализацию всех этих примеров и фрагментов кода можно найти на GitHub .