Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll explore different methods of converting Jackson’s raw data type JsonNode into typed Java collections. While we can read JSON using JsonNode itself, converting it to a Java collection can be beneficial. Java collections provide advantages over raw JSON data such as type safety, faster processing, and availability of more type-specific operations.

2. Example Setup

In our code example, we’ll look at different ways to convert a JsonNode to a List or Map of objects. Let’s set up the building blocks of our example.

2.1. Dependency

To start with, let’s add the Jackson Core dependency to the pom.xml file:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.17.0</version>
</dependency>

2.2. JSON Data

Next, let’s define a JSON for our use case:

{
    "persons": [
        {
            "name": "John",
            "age": 30
        },
        {
            "name": "Alice",
            "age": 25
        }
    ],
    "idToPerson" : {
        "1234": {
            "name": "John",
            "age": 30
        },
        "1235": {
            "name": "Alice",
            "age": 25
        }
    }
}

In the above JSON, we have a JSON array persons and a JSON object idToPerson. We’ll look at methods to convert them to Java collections.

2.3. The DTO

Let’s define a Person class that we can use in our example:

public class Person {
    private String name;
    private int age;
    
    // constructors/getters/setters
}

2.4. Converting JSON String to JsonNode

If we want to read an object from the entire JSON, we can use the ObjectMapper class of Jackson to do so:

JsonNode rootNode = new ObjectMapper().readTree(jsonString);
JsonNode childNode = rootNode.get("persons");

To convert the entire JSON into a JsonNode object, we use the readTree() method. We then traverse the JsonNode object using the get() method that returns the nested object with the specified name.

3. Manual Conversion

Before checking library methods, let’s look at a way to convert a JsonNode into a collection manually.

3.1. Manually Converting JsonNode to List

To convert a JsonNode to a list, we can traverse it entry by entry and create a List object with it:

List<Person> manualJsonNodeToList(JsonNode personsNode) {
    List<Person> people = new ArrayList<>();
    for (JsonNode node : personsNode) {
        Person person = new Person(node.get("name").asText(), node.get("age").asInt());
        people.add(person);
    }

    return people;
}

Here, we use a loop to traverse all children of the input node. This is possible only if our input node is an array.

For each node, we create a Person object and add it to the list. We get name and age from the node using the get(fieldName) method. JsonNode provides various methods to convert the returned value into primitive Java types. Here, the asText() and asInt() methods convert the value to String and int, respectively.

3.2. Manually Converting JsonNode to Map

Let’s look at a similar conversion for the map:

Map<String, Person> manualJsonNodeToMap(JsonNode idToPersonsNode) {
    Map<String, Person> mapOfIdToPerson = new HashMap<>();
    idToPersonsNode.fields()
      .forEachRemaining(node -> mapOfIdToPerson.put(node.getKey(),
        new Person(node.getValue().get("name").asText(), node.getValue().get("age").asInt())));

    return mapOfIdToPerson;
}

Here, we use the fields() method to iterate over map entries. It returns an Iterator<Map.Entry<String, JsonNode>> object that we can process further. Next, we read each entry and put it into our Map.

4. Using Jackson’s readValue() and convertValue()

Jackson provides multiple methods to convert JsonNode into Java objects. Let’s look at two of them.

4.1. Using readValue()

The readValue() method can be used to convert to List or Map using a TypeReference:

List<Person> readValueJsonNodeToList(JsonNode personsNode) throws IOException {
    TypeReference<List<Person>> typeReferenceList = new TypeReference<List<Person>>() {};
    return new ObjectMapper().readValue(personsNode.traverse(), typeReferenceList);
}

Map<String, Person> readValueJsonNodeToMap(JsonNode idToPersonsNode) throws IOException {
    TypeReference<Map<String, Person>> typeReferenceMap = new TypeReference<Map<String, Person>>() {};
    return new ObjectMapper().readValue(idToPersonsNode.traverse(), typeReferenceMap);
}

First, we create a TypeReference object by passing the exact type we need to convert to. We then call the readValue() method where JsonParser is provided by jsonNode.traverse(). Using the parser, it deserializes the node to a list or map as per the TypeReference we provide.

4.2. Using convertValue()

Similarly, we can use the convertValue() method:

List<Person> convertValueJsonNodeToList(JsonNode personsNode) {
    TypeReference<List<Person>> typeReferenceList = new TypeReference<List<Person>>() {};
    return new ObjectMapper().convertValue(personsNode, typeReferenceList);
}

Map<String, Person> convertValueJsonNodeToMap(JsonNode idToPersonsNode) {
    TypeReference<Map<String, Person>> typeReferenceMap = new TypeReference<Map<String, Person>>() {};
    return new ObjectMapper().convertValue(idToPersonsNode, typeReferenceMap);
}

The convertValue() method works by first serializing the input object and then deserializing it to the desired type. Therefore, it can be used more flexibly for converting from one object to another. For example, we could also use it for reverse comparison from a Java object to JsonNode.

5. Custom Deserializer

We can also provide a custom deserializer to perform the conversion. Let’s look at how we can define one:

public class CustomPersonListDeserializer extends JsonDeserializer<List<Person>> {
    @Override
    public List<Person> deserialize(com.fasterxml.jackson.core.JsonParser p, 
      com.fasterxml.jackson.databind.DeserializationContext ctxt) throws IOException {
        ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
        List<Person> personList = new ArrayList<>();
        JsonNode personsNode = objectMapper.readTree(p);
        for (JsonNode node : personsNode) {
            personList.add(objectMapper.readValue(node.traverse(), Person.class));
        }

        return personList;
    }
}

Let’s look at a few important parts of the code:

  • First, the class extends Jackson’s JsonDeserializer.
  • Then, we override the deserialize() method and provide our implementation.
  • In the implementation, we get the ObjectMapper from the JsonParser object.
  • objectMapper.readTree() converts the entire tree represented by the parser into a JsonNode instance.
  • Finally, similar to the manual conversion, we convert each node in the JSON array to a Person object by looping over the nodes.

The deserializer works similarly to other methods, except it can provide a separation of concern. Additionally, custom deserializers provide flexibility as we can easily switch between deserializers in the calling code.

We’ll look at how to use the deserializer when we test the code in the next section.

6. Testing

Now that we have our different methods ready, let’s write some tests to verify them.

6.1. Setup

Let’s set up our test class:

public class JsonNodeToCollectionUnitTest {

    public static String jsonString = "{\"persons\":[{\"name\":\"John\",\"age\":30},{\"name\":\"Alice\",\"age\":25}],\"idToPerson\":{\"1234\":{\"name\":\"John\",\"age\":30},\"1235\":{\"name\":\"Alice\",\"age\":25}}}";

    static JsonNode completeJson;
    static JsonNode personsNode;
    static JsonNode idToPersonNode;

    @BeforeAll
    static void setup() throws JsonProcessingException {
        completeJson = new ObjectMapper().readTree(jsonString);
        personsNode = completeJson.get("persons");
        idToPersonNode = completeJson.get("idToPerson");
    }
}

Here, we define a JSON string that we can use as a test input. Then, we define a setup() method that executes before all tests. It sets up our input JsonNode objects.

6.2. Testing Conversion Methods

Next, let’s test our conversion methods:

@Test
void givenJsonNode_whenConvertingToList_thenFieldsAreCorrect() throws IOException {

    List<Person> personList1 = JsonNodeConversionUtil.manualJsonNodeToList(personsNode);
    List<Person> personList2 = JsonNodeConversionUtil.readValueJsonNodeToList(personsNode);
    List<Person> personList3 = JsonNodeConversionUtil.convertValueJsonNodeToList(personsNode);

    validateList(personList1);
    validateList(personList2);
    validateList(personList3);
}

private void validateList(List<Person> personList) {
    assertEquals(2, personList.size());

    Person person1 = personList.get(0);
    assertEquals("John", person1.getName());
    assertEquals(30, person1.getAge());

    Person person2 = personList.get(1);
    assertEquals("Alice", person2.getName());
    assertEquals(25, person2.getAge());
}

Here, we call each method and verify that the contents of the returned list are as we expected.

Let’s add a similar test for map conversions:

@Test
void givenJsonNode_whenConvertingToMap_thenFieldsAreCorrect() throws IOException {

    Map<String, Person> personMap1 = JsonNodeConversionUtil.manualJsonNodeToMap(idToPersonNode);
    Map<String, Person> personMap2 = JsonNodeConversionUtil.readValueJsonNodeToMap(idToPersonNode);
    Map<String, Person> personMap3 = JsonNodeConversionUtil.convertValueJsonNodeToMap(idToPersonNode);

    validateMapOfPersons(personMap1);
    validateMapOfPersons(personMap2);
    validateMapOfPersons(personMap3);
}

private void validateMapOfPersons(Map<String, Person> map) {
    assertEquals(2, map.size());

    Person person1 = map.get("1234");
    assertEquals("John", person1.getName());
    assertEquals(30, person1.getAge());

    Person person2 = map.get("1235");
    assertEquals("Alice", person2.getName());
    assertEquals(25, person2.getAge());
}

6.3. Testing Custom Deserializer

Let’s see how to use the custom deserializer and then compare the results:

@Test
void givenJsonNode_whenConvertingToListWithCustomDeserializer_thenFieldsAreCorrect() throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addDeserializer(List.class, new CustomPersonListDeserializer());
    objectMapper.registerModule(module);
    List<Person> personList = objectMapper.convertValue(personsNode, new TypeReference<List<Person>>() {
    });

    validateList(personList);
}

Here, we first wrap the CustomPersonListDeserializer in a module using the SimpleModule class. Then, we register this module with the ObjectMapper instance. By doing this, we instruct the ObjectMapper to use CustomPersonListDeserializer whenever a List needs to be deserialized.

When we call the convertValue() method next, the deserializer is used to return the output list.

7. Conclusion

In this article, we explored how to convert Jackson’s JsonNode into typed Java collections like List and Map. We discussed multiple methods to perform this operation and tested that they all provide the same output.

For small conversions, we may use the manual method. As the JSON size grows, using methods from Jackson’s library would be better. And if a more complex conversion is required, it would be better to provide our custom deserializer.

As usual, the source code for the examples can be found over on GitHub.

Course – LS (cat=JSON/Jackson)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments