Jackson Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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 creation of a node is to instantiate an ObjectMapper object by using the default constructor:

ObjectMapper mapper = new ObjectMapper();

Since creation of an ObjectMapper object is expensive, it's recommended that the same one should be reused 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

The most common way to create a node out of nothing is as follows:

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 if you need 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 us 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

The basic method to transform a tree node into a JSON string is the following:

mapper.writeValue(destination, node);

where the destination can be a File, an OutputStream or a Writer.
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);

Which is functionally equivalent to:

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

The following JSON elements, contained in a file named example.json, are used as a base structure for actions discussed in this section 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 sub-sections.

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 the path to the node is known beforehand, that's pretty easy to do. For example, 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 5. Iterating Over the Nodes

4.2. Adding a New Node

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

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 will 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<String> 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 node, 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 node 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 as 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 as 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. 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. 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 tutorial covered the common APIs and scenarios of working with a tree model in Jackson.

And, as always, the implementation of all these examples and code snippets can be found in over on GitHub – this is a Maven based project, so it should be easy to import and run as it is.

Jackson bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
newest oldest most voted
Notify of
Rishi
Guest
Rishi

Very well explained. Thanks.
How would you get a path(or key) hierarchy. Say for following Json:
{
“contactDetails”: [
{
“title”: “Jackson founder”,
“company”: “FasterXML”,
“name”:
{
“first”: “Tatu”,
“last”: “Saloranta”
},
“contactNumber”:
{
// edited out
}
}
]
}

How could I get the following text:
contactDetails[1].contactNumber.phone.landline OR contactDetails[1]/contactNumber/phone/landline
( dot or / is just a separator)
Kindly help.

Eugen Paraschiv
Guest

Hey Rishi – this looks like an interesting piece of JSON to parse; I’m editing out most of the example (to long for the comments section) – but feel free to open a PR with a failing test on the main repo of the article and I’ll be happy to have a look.
Cheers,
Eugen.

Comments are closed on this article!