Read and write JSON data using Jackson low-level methods

Published by Alexander Braun on 16 Apr 2019 - tagged with Java

Most of the time it is beneficial to parse and modify JSON data using an underlying Java object. But, there are some scenarios where parsing the entire data structure is not necessary and thus, low-level read and write methods from the Jackson library might be more efficient. In this post I will evaluate how these low-level methods can be used.

Motivation

Most of the time we can and should work with JSON data, by mapping the raw JSON data to Java classes. This sometimes involves a lot of work, especially when no JSON Schema has been provided and the model classes either have to be implemented manually or a JSON schema has to be created upfront. There are some scenarios when working directly with the raw data might be easier and more efficient, e.g.:

  • The underlying JSON structure is huge, but you only have to read a few parameters
  • The JSON data you have to process changes frequently, but you are only interested in a stable subset of data
  • No JSON schema file is available
  • Only a few attributes in the data have to be modified and the data should be stored as is after modification

You can find the complete code for this project at github.

Example JSON data

We will use the following JSON data to implement some examples.

{
  "firstName": "John",
  "lastName": "Doe",
  "middleName": null,
  "score": 8.7,
  "active": true,
  "personalInfo": {
    "dateOfBirth": "1996-10-18",
    "emails": [
      {
        "email": "personal.email@example.com",
        "type": "personal",
        "active": true
      },
      {
        "email": "business.email@example.com",
        "type": "business",
        "active": false
      }
    ]
  }
}

Creating the ObjectMapper instance

To use Jackson, we have to create an instance of ObjectMapper. I usually add some configuration for handling date and time. You can find the entire utility class with all examples here. Please also have a look into the JUnit tests that explain how the utility class can be used and show different scenarios.

public class JsonUtils {

    private static ObjectMapper objectMapper;

    static {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    // Other methods will be implemented and explained below

}

Parse JSON data

Before we can read or write data from/to a JSON data structure, we have to use objectMapper.readTree(String jsonAsString) to parse the raw data string. This method returns a JsonNode, which basically is our root node or entry point to the data structure.

    /**
     * Parses the given string and returns the corresponding {@link JsonNode} representation.
     * @param jsonAsString the JSON data as String
     * @return the corresponding {@link JsonNode}
     * @throws JsonMappingException
     * @throws JsonProcessingException
     */
    public static JsonNode getJsonStringAsNode(String jsonAsString)
        throws JsonMappingException, JsonProcessingException {
        return objectMapper.readTree(jsonAsString);
    }

Read data from the JSON data structure

The following section shows different methods to read data.

Get node by name

The easiest way to read an attribute is to use jsonNode.findPath(String fieldname).

    /**
     * Looks up the field name in the given {@link JsonNode}. The method will return the first
     * occurrence of the field name. Thus, most of the time it is better to
     * use {@link #getNodeByPath(JsonNode, String)} when the entire path is known.
     * @param jsonNode the {@link JsonNode} to search within
     * @param fieldname the name of the field to search for
     * @return an {@link Optional} of type {@link JsonNode}
     */
    public static Optional<JsonNode> getNodeByName(JsonNode jsonNode, String fieldname) {
        JsonNode node = jsonNode.findPath(fieldname);
        if(node.isMissingNode()) {
            return Optional.empty();
        }
        return Optional.of(node);
    }

While it is straight-forward, this approach has a disadvantage - the method returns the first node with the given fieldname. To demonstrate this behavior, let's have a look at the test below.

    @Test
    public void testGetNodeByName() throws JsonMappingException, JsonProcessingException {
        JsonNode node = JsonUtils.getJsonStringAsNode(getSimpleJsonString());
        // Scenario 1: dateOfBirth should be found
        JsonNode dateOfBirth = JsonUtils.getNodeByName(node, "dateOfBirth").orElse(null);
        assertNotNull(dateOfBirth);
        assertEquals("1996-10-18", dateOfBirth.asText());
        // Scenario 2: middle name should be null
        JsonNode middleName = JsonUtils.getNodeByName(node, "middleName").orElse(null);
        assertTrue(middleName.isNull());
        // Scenario 3: email from array returns first value
        JsonNode email = JsonUtils.getNodeByName(node, "email").orElse(null);
        assertEquals("personal.email@example.com", email.asText());
    }

In scenario 3, we are looking for a node named "email". The example JSON data has several fields named "email", but with this approach only the first node has been returned.

The method is still useful, when we are sure that a fieldname is only being used once or in case we are only interested in the first occurrence of a particular field.

Get node by path

To have better control which node should be returned, we can use the entire path to search. Based on the JsonUtils class, the path has to start with / and all sub-paths are separated by / as well. To get the dateOfBirth node, we have to use /personalInfo/dateOfBirth. To ensure that only existing JsonNodes are being returned we have to use node.isMissingNode(). Internally the class uses jsonNode.at(String path) to retrieve the node we are interested in.

    /**
     * Looks up a {@link JsonNode} based on the given path. The path expression has to start with
     * "/" and each sub-path ahs to be separated with "/" as well. Example: "/personalInfo/dateOfBirth".
     * @param jsonNode the {@link JsonNode} to search within
     * @param path the path expression, e.g. "/personalInfo/dateOfBirth"
     * @return an {@link Optional} of type {@link JsonNode}
     */
    public static Optional<JsonNode> getNodeByPath(JsonNode jsonNode, String path) {
        JsonNode node = jsonNode.at(path);
        if(node.isMissingNode()) {
            return Optional.empty();
        }
        return Optional.of(node);
    }

And again, here is the corresponding test case:

    @Test
    public void testGetNodeByPath() throws JsonMappingException, JsonProcessingException {
        JsonNode node = JsonUtils.getJsonStringAsNode(getSimpleJsonString());
        // Scenario 1: path to an existing node
        assertEquals("1996-10-18", JsonUtils.getNodeByPath(node, "/personalInfo/dateOfBirth").get().textValue());
        // Scenario 2: path to a non-existing node
        assertFalse(JsonUtils.getNodeByPath(node, "/personalInfo/address/street").isPresent());
    }

Get value as String

In this example we use jsonNode.asText() to get the string value of an attribute. There are many different methods for retrieving Integer, Long, Double and Boolean values. It is important, though, to check if the node is of a particular type before invoking the corresponding method - e.g. with jsonNode.isTextual().

    /**
     * Looks up and returns the string value of the {@link JsonNode} specified by the path.
     * @param jsonNode the root {@link JsonNode}
     * @param path the path to the {@link JsonNode} to get the string value for
     * @return an {@link Optional} of type {@link JsonNode}
     */
    public static Optional<String> getValueAsString(JsonNode jsonNode, String path) {
        Optional<JsonNode> textNode = getNodeByPath(jsonNode, path);
        if(textNode.isPresent() && textNode.get().isTextual()) {
            return Optional.of(textNode.get().asText());
        }
        return Optional.empty();
    }

Below are two test scenarios to cover the behaviour for String and Boolean types.

    @Test
    public void testGetValueAsString() throws JsonMappingException, JsonProcessingException {
        JsonNode node = JsonUtils.getJsonStringAsNode(getSimpleJsonString());
        // Scenario 1: A string value can be retrieved
        assertEquals("John", JsonUtils.getValueAsString(node, "/firstName").get());
        // Scenario 2: "active" is a boolean, thus an empty Optional will be returned
        assertFalse(JsonUtils.getValueAsString(node, "/active").isPresent());
    }

Get array node

The last method I'd like to show is how to retrieve ArrayNodes. We can use the getNodeByPath(JsonNode jsonNode, String path) method we used earlier to retrieve an array. Then we have to check if the returned JsonNode is an ArrayNode, cast the JsonNode to ArrayNode and finally return it.

    /**
     * Tries to locate an {@link ArrayNode} at the given path.
     * @param jsonNode the root {@link JsonNode}
     * @param path the path to the {@link ArrayNode}
     * @return an {@link Optional} of type {@link ArrayNode}
     */
    public static Optional<ArrayNode> getArrayNode(JsonNode jsonNode, String path) {
        Optional<JsonNode> arrayNode = getNodeByPath(jsonNode, path);
        if(!arrayNode.isPresent() || !arrayNode.get().isArray()) {
            return Optional.empty();
        }
        return Optional.of((ArrayNode)arrayNode.get());
    }

The test code below shows how to ultimately access the values of an array. There are also methods to iterate through the array.

    @Test
    public void testGetArrayNode() throws JsonMappingException, JsonProcessingException {
        JsonNode node = JsonUtils.getJsonStringAsNode(getSimpleJsonString());
        ArrayNode arrayNode = JsonUtils.getArrayNode(node, "/personalInfo/emails").get();
        assertEquals("personal.email@example.com", arrayNode.get(0).get("email").asText());
        assertTrue(arrayNode.get(0).get("active").asBoolean());
        assertEquals("business.email@example.com", arrayNode.get(1).get("email").asText());
        assertFalse(arrayNode.get(1).get("active").asBoolean());
    }

This concludes the section about how to retrieve nodes and how to access the data.

In the next section we will cover how to add or update a node.

Write data to a JSON structure

The last topic to cover in this post is to modify the value of an existing node and to add a new string node into the data structure. The setOrUpdateTextValue method does exactly this.

  • We use the given path and get the last index of /. This allows us to get the path for the parent node and the new attribute name to add
  • Then we lookup the parent node and check if it points to an ObjectNode
  • We also check if the attribute already exists and if yes we remove the node
  • Finally we add a new text node to the parent node
    /**
     * Creates or updates the string value of an element described by path.
     * @param jsonNode the root {@link JsonNode}
     * @param path the path to the {@link JsonNode} to set the string value for
     * @param stringValue the value t oset
     * @return true if the value could be set
     */
    public static boolean setOrUpdateTextValue(JsonNode jsonNode, String path, String stringValue) {
        // Extract parentPath and attribute name, given the path variable looks like
        // /root/sub/sub/attributeName
        int lastSeparator = path.lastIndexOf("/");
        String attributeName = path.substring(lastSeparator+1);
        JsonNode parentNode = getNodeByPath(jsonNode, path.substring(0, lastSeparator)).orElse(null);

        // We have to ensure that the parent node exist and that the parent node is an object
        if(parentNode == null || !parentNode.isObject()) {
            return false;
        }

        // If the attribute already exists, we have to first remove it
        if(parentNode.hasNonNull(attributeName)) {
            ((ObjectNode)parentNode).remove(attributeName);
        }
        // Add the attribute and it's value
        ((ObjectNode)parentNode).put(attributeName, stringValue);
        return true;
    }

And finally, below is the test case to check if it works as expected:

    @Test
    public void testGetArrayNode() throws JsonMappingException, JsonProcessingException {
        JsonNode node = JsonUtils.getJsonStringAsNode(getSimpleJsonString());
        ArrayNode arrayNode = JsonUtils.getArrayNode(node, "/personalInfo/emails").get();
        assertEquals("personal.email@example.com", arrayNode.get(0).get("email").asText());
        assertTrue(arrayNode.get(0).get("active").asBoolean());
        assertEquals("business.email@example.com", arrayNode.get(1).get("email").asText());
        assertFalse(arrayNode.get(1).get("active").asBoolean());
    }

That's all I wanted to cover today. There are many more topics and methods Jackson can help with parsing and writing JSON data. In case you are interested, I encourage you to go through the API and check what else it has to offer.