Read and write JSON data using Jackson low-level methods
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 JsonNode
s 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 ArrayNode
s. 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.
Tags
AOP Apache Kafka Bootstrap Go Java Linux MongoDB Nginx Security Spring Spring Boot Spring Security SSL ThymeleafSearch
Archive
- 1 December 2023
- 1 November 2023
- 1 May 2019
- 2 April 2019
- 1 May 2018
- 1 April 2018
- 1 March 2018
- 2 February 2018
- 1 January 2018
- 5 December 2017
- 7 November 2017
- 2 October 2017