1. Overview

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

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

2. Creating a Node

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

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.

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

2.1. Construct a Node from Scratch

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:

JsonNode node = JsonNodeFactory.instance.objectNode();

2.2. Parse from a JSON Source

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

2.3. Convert from an Object

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

JsonNode node = mapper.valueToTree(fromValue);

The convertValue API is also helpful here:

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

Let’s see how it works in practice.

Assume we have a class named 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.1. Write out as 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:

mapper.writeValue(destination, node);

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

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

The most convenient way to convert a JsonNode into a Java object is the 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

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

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

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

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

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

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:

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

Alternatively, the get or with APIs can also be used instead of 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.

4.2. Adding a New Node

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.

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

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

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

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

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

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:

@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

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

JsonNode removedNode = locatedNode.remove(fieldName);

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

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.

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

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

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

So, let’s ensure our sample data has all three different types by adding an 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:

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.

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

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

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

@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

We need to handle different types of nodes slightly differently.

We’ll do this in our processNode method:

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.

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.

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

Finally, we want to iterate over all the child nodes of each Object node. One option is to use 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:

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:

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:

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.

6. Conclusion

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

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


» 下一篇: Java周报,109期