Working with Tree Model Nodes in Jackson – 在杰克逊中使用树状模型节点

最后修改: 2016年 1月 25日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

This tutorial will focus on working with tree model nodes in Jackson.

本教程将重点介绍在Jackson中使用树模型节点的工作。

We’ll use JsonNode for various conversions as well as adding, modifying, and removing nodes.

我们将使用JsonNode进行各种转换,以及添加、修改和删除节点。

2. Creating a Node

2.创建一个节点

The first step in the creation of a node is to instantiate an ObjectMapper object by using the default constructor:

创建节点的第一步是通过使用默认构造函数来实例化一个ObjectMapper对象。

ObjectMapper mapper = new ObjectMapper();

Since the creation of an ObjectMapper object is expensive, it’s recommended that we reuse the same one for multiple operations.

由于创建一个ObjectMapper对象的成本很高,建议我们在多次操作中重复使用同一个对象。

Next, we have three different ways to create a tree node once we have our ObjectMapper.

接下来,一旦我们有了ObjectMapper,我们有三种不同的方式来创建一个树节点。

2.1. Construct a Node from Scratch

2.1.从零开始构建一个节点

This is the most common way to create a node out of nothing:

这是最常见的无中生有的创建节点的方法。

JsonNode node = mapper.createObjectNode();

Alternatively, we can also create a node via the JsonNodeFactory:

另外,我们也可以通过JsonNodeFactory创建一个节点。

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Parse from a JSON Source

2.2.从JSON源解析

This method is well covered in the Jackson – Marshall String to JsonNode article. Please refer to it for more info.

这个方法在Jackson – Marshall String to JsonNode一文中有详细介绍。请参考它以获取更多信息。

2.3. Convert from an Object

2.3.从一个对象转换

A node may be converted from a Java object by calling the valueToTree(Object fromValue) method on the ObjectMapper:

通过调用ObjectMapper上的valueToTree(Object fromValue)方法,一个节点可以从一个Java对象转换。

JsonNode node = mapper.valueToTree(fromValue);

The convertValue API is also helpful here:

convertValueAPI在这里也有帮助。

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

Let’s see how it works in practice.

让我们看看它在实践中如何运作。

Assume we have a class named NodeBean:

假设我们有一个名为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
}

Let’s write a test that makes sure that the conversion happens correctly:

让我们写一个测试,确保转换正确发生。

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

    JsonNode node = mapper.valueToTree(fromValue);

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

3. Transforming a Node

3.改造一个节点

3.1. Write out as JSON

3.1.以JSON格式写出来

This is the basic method to transform a tree node into a JSON string, where the destination can be a File, an OutputStream or a Writer:

这是将树节点转化为JSON字符串的基本方法,目的地可以是文件OutputStreamWriter

mapper.writeValue(destination, node);

By reusing the class NodeBean declared in Section 2.3, a test makes sure this method works as expected:

通过重用第2.3节中声明的NodeBean类,一个测试可以确保这个方法按预期工作。

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. Convert to an Object

3.2.转换为一个对象

The most convenient way to convert a JsonNode into a Java object is the treeToValue API:

JsonNode转换为Java对象的最方便方法是treeToValue API。

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

This is functionally equivalent to the following:

这在功能上等同于以下内容。

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

We can also do that through a token stream:

我们也可以通过代币流来实现。

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

Finally, let’s implement a test that verifies the conversion process:

最后,让我们实现一个验证转换过程的测试。

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

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

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

4. Manipulating Tree Nodes

4.操纵树形节点

We’ll use the following JSON elements, contained in a file named example.json, as a base structure for actions to be taken on:

我们将使用以下JSON元素,包含在一个名为example.json的文件中,作为要采取的行动的基础结构。

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

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

This JSON file, located on the classpath, is parsed into a model tree:

这个位于classpath上的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;
    }
}

Note that the root of the tree will be used when illustrating operations on nodes in the following subsections.

注意,在下面的小节中说明对节点的操作时,将使用树的根。

4.1. Locating a Node

4.1.定位一个节点

Before working on any node, the first thing we need to do is to locate and assign it to a variable.

在对任何节点进行处理之前,我们需要做的第一件事是定位并将其分配给一个变量。

If we know the path to the node beforehand, that’s pretty easy to do.

如果我们事先知道通往节点的路径,这就很容易做到。

Say we want a node named last, which is under the name node:

假设我们想要一个名为last的节点,它在name节点下面。

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

Alternatively, the get or with APIs can also be used instead of path.

另外,也可以使用getwith API来代替path

If the path isn’t known, the search will, of course, become more complex and iterative.

如果路径不知道,搜索当然就会变得更加复杂和反复。

We can see an example of iterating over all the nodes in Section 5 – Iterating Over the Nodes.

我们可以在第5节 – 遍历节点中看到遍历所有节点的例子。

4.2. Adding a New Node

4.2.添加一个新节点

A node can be added as a child of another node:

一个节点可以作为另一个节点的子节点被添加。

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

Many overloaded variants of put may be used to add new nodes of different value types.

put的许多重载变体可用于添加不同价值类型的新节点。

Many other similar methods are also available, including putArray, putObject, PutPOJO, putRawValue and putNull.

还有许多其他类似的方法,包括putArrayputObjectPutPOJOputRawValueputNull

Finally, let’s have a look at an example where we add an entire structure to the root node of the tree:

最后,让我们看看一个例子,我们在树的根节点上添加一个完整的结构。

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

Here’s the full test going through all of these operations and verifying the results:

下面是完整的测试,经历了所有这些操作并验证了结果。

@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. Editing a Node

4.3.编辑一个节点

An ObjectNode instance may be modified by invoking set(String fieldName, JsonNode value) method:

一个ObjectNode实例可以通过调用set(String fieldName, JsonNode value)方法被修改。

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

Similar results might be achieved by using replace or setAll methods on objects of the same type.

通过在相同类型的对象上使用replacesetAll方法,可以实现类似的结果。

To verify that the method works as expected, we’ll change the value of the field name under root node from an object of first and last into another one consisting of only nick field in a test:

为了验证该方法是否按预期工作,我们将在测试中把根节点下的name字段的值从firstlast的对象变为另一个仅由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. Removing a Node

4.4.删除一个节点

A node can be removed by calling the remove(String fieldName) API on its parent node:

一个节点可以通过在其父节点上调用remove(String fieldName) API而被移除。

JsonNode removedNode = locatedNode.remove(fieldName);

In order to remove multiple nodes at once, we can invoke an overloaded method with the parameter of Collection<String> type, which returns the parent node instead of the one to be removed:

为了一次删除多个节点,我们可以调用一个重载方法,参数为Collection<String>类型,它返回父节点而不是要删除的节点。

ObjectNode locatedNode = locatedNode.remove(fieldNames);

In the extreme case when we want to delete all subnodes of a given node, the removeAll API comes in handy.

在极端情况下,当我们想删除一个给定节点的所有子节点时, removeAll API派上用场。

The following test will focus on the first method mentioned above, which is the most common scenario:

下面的测试将侧重于上述第一种方法,这是最常见的情况。

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

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

5. Iterating Over the Nodes

5.迭代节点

Let’s iterate over all the nodes in a JSON document and reformat them into YAML.

让我们遍历JSON文档中的所有节点,并将它们重新格式化为YAML。

JSON has three types of nodes, which are Value, Object and Array.

JSON有三种类型的节点,分别是Value、Object和Array。

So, let’s ensure our sample data has all three different types by adding an Array:

因此,让我们通过添加一个Array来确保我们的样本数据拥有所有三种不同的类型。

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

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

Now let’s see the YAML we want to produce:

现在让我们看看我们想产生的YAML。

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

We know that JSON nodes have a hierarchical tree structure. So, the easiest way to iterate over the whole JSON document is to start at the top and work our way down through all the child nodes.

我们知道,JSON节点有一个分层的树状结构。因此,对整个JSON文档进行迭代的最简单方法是从顶部开始,然后通过所有的子节点向下进行。

We’ll pass the root node into a recursive method. The method will then call itself with each child of the supplied node.

我们将把根节点传递给一个递归方法。然后,该方法将通过提供的节点的每个子节点来调用自己。

5.1. Testing the Iteration

5.1.测试迭代

We’ll start by creating a simple test that checks that we can successfully convert the JSON to YAML.

我们将首先创建一个简单的测试,检查我们是否能成功地将JSON转换为YAML。

Our test supplies the root node of the JSON document to our toYaml method and asserts the returned value is what we expect:

我们的测试将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. Handling Different Node Types

5.2.处理不同的节点类型

We need to handle different types of nodes slightly differently.

我们需要以稍微不同的方式处理不同类型的节点。

We’ll do this in our processNode method:

我们将在我们的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);
    }
}

First, let’s consider a Value node. We simply call the asText method of the node to get a String representation of the value.

首先,让我们考虑一个Value节点。我们简单地调用节点的asText方法来获得值的String表示。

Next, let’s look at an Array node. Each item within the Array node is itself a JsonNode, so we iterate over the Array and pass each node to the appendNodeToYaml method. We also need to know that these nodes are part of an array.

接下来,让我们看看一个数组节点。数组节点中的每个项目本身就是一个JsonNode,所以我们遍历数组并将每个节点传递给appendNodeToYaml方法。我们还需要知道,这些节点是数组的一部分。

Unfortunately, the node itself does not contain anything that tells us that, so we’ll pass a flag into our appendNodeToYaml method.

不幸的是,节点本身并不包含任何告诉我们的内容,所以我们要在appendNodeToYaml方法中传递一个标志。

Finally, we want to iterate over all the child nodes of each Object node. One option is to use JsonNode.elements.

最后,我们要遍历每个Object节点的所有子节点。一个选择是使用JsonNode.elements

However, we can’t determine the field name from an element because it just contains the field value:

然而,我们不能从一个元素中确定字段名,因为它只包含字段值。

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

Instead, we’ll use JsonNode.fields because this gives us access to both the field name and value:

相反,我们将使用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}]

For each field, we add the field name to the output and then process the value as a child node by passing it to the processNode method:

对于每个字段,我们将字段名添加到输出中,然后通过将其传递给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;
    }
        
}

We can’t tell from the node how many ancestors it has.

我们无法从节点上看出它有多少个祖先。

So, we pass a field called depth into the processNode method to keep track of this, and we increment this value each time we get a child node so that we can correctly indent the fields in our YAML output:

因此,我们在processNode方法中传递了一个名为depth的字段,以跟踪这一点,并且每次获得一个子节点时,我们都会递增这个值,这样我们就可以在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(": ");
}

Now that we have all the code in place to iterate over the nodes and create the YAML output, we can run our test to show that it works.

现在我们已经有了所有的代码来迭代节点并创建YAML输出,我们可以运行我们的测试来显示它的工作。

6. Conclusion

6.结论

This article covered the common APIs and scenarios while working with a tree model in Jackson.

这篇文章涵盖了在Jackson中使用树状模型时常见的API和场景。

And as always the implementation of all these examples and code snippets can be found over on GitHub.

和以往一样,所有这些例子和代码片断的实现都可以在GitHub上找到