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 look at the @JsonMerge annotation from the Jackson Java library. Jackson is well known for providing the ability to work with JSON within our Java applications. This annotation allows us to merge new data into an object within a nested POJO (plain old Java object) or Map. We’ll look at the existing functionality without the annotation and then see what difference it makes when we use it in our code.

2. What @JsonMerge Does

One of the most frequently used Jackson features is the ObjectMapper which allows us to map JSON into our Java objects and do the same in reverse. One ability of the ObjectMapper is to read an object and update it with new data from a JSON String, assuming the JSON is in the correct structure. Before the introduction of @JsonMerge, a limitation of that updating capability was that it would overwrite POJOs and Maps. With this annotation, properties within nested POJOs and Maps are merged in the update.

Let’s look at how to use @JsonMerge in practice. We’ll create two objects, firstly a Keyboard:

class Keyboard {
    String style;
    String layout;
    // Standard getters, setters and constructors
}

Secondly, the Programmer who will use the Keyboard:

class ProgrammerNotAnnotated {
    String name;
    String favouriteLanguage;
    Keyboard keyboard;
    // Standard getters, setters and constructors
}

Later on, we’ll add in the @JsonMerge annotation, but for now, we’re ready.

3. Merging Without @JsonMerge

To update an object, we first need the JSON String to represent the new data we want to merge in:

String newData = "{\"favouriteLanguage\":\"Java\",\"keyboard\":{\"style\":\"Mechanical\"}}";

We then need to create the object we want to update with our new data:

ProgrammerNotAnnotated programmerToUpdate = new ProgrammerNotAnnotated("John", "C++", new Keyboard("Membrane", "US"));

Let’s use the String and object we’ve just defined and see what happens without the annotation. We’ll create an instance of ObjectMapper first and use it to create an ObjectReaderObjectReader is a lightweight, thread-safe object we can use for a lot of the same functionality as an ObjectMapper with fewer overheads. We can use ObjectReader instances on a per serialization/deserialization basis because they are so cheap to make and configure.

We’ll create the ObjectReader with ObjectMapper.readerForUpdating(), passing in the object we want to update as the only argument. This is a factory method specifically for returning an ObjectReader instance that will update the given object with new data from a JSON String. Once we have our ObjectReader, we simply call readValue() and pass in our new data:

@Test
void givenAnObjectAndJson_whenNotUsingJsonMerge_thenExpectNoUpdateInPOJO() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    ObjectReader objectReader = objectMapper.readerForUpdating(programmerToUpdate);
    ProgrammerNotAnnotated update = objectReader.readValue(newData);

    assert(update.getFavouriteLanguage()).equals("Java");
    assertNull(update.getKeyboard()
      .getLayout());
}

Afterward, we can print out update to see what we end up with clearly:

{name='John', favouriteLanguage='Java', keyboard=Keyboard{style='Mechanical', layout='null'}}

We can see from the test assertions and the JSON that our programmerToUpdate received top-level updates, his favorite language is now Java. However, we have completely overwritten the nested Keyboard object, and even though the new data only contained a style, we have lost the layout property. The ability to merge POJOs like Keyboard is one of the main benefits of the @JsonMerge annotation, as we’ll see in the next section.

4. Merging With @JsonMerge

Let’s now make a new Programmer object with the @JsonMerge annotation:

class ProgrammerAnnotated {
    String name;
    String favouriteLanguage;
    @JsonMerge
    Keyboard keyboard;
    // Standard getters, setters and constructors
}

If we use that object in the same way as above, we get a different result:

@Test
void givenAnObjectAndJson_whenUsingJsonMerge_thenExpectUpdateInPOJO() throws JsonProcessingException {
    String newData = "{\"favouriteLanguage\":\"Java\",\"keyboard\":{\"style\":\"Mechanical\"}}";
    ProgrammerAnnotated programmerToUpdate = new ProgrammerAnnotated("John", "C++", new Keyboard("Membrane", "US"));

    ObjectMapper objectMapper = new ObjectMapper();
    ProgrammerAnnotated update = objectMapper.readerForUpdating(programmerToUpdate).readValue(newData);

    assert(update.getFavouriteLanguage()).equals("Java");
    // Only works with annotation
    assert(update.getKeyboard().getLayout()).equals("US");
}

Finally, we can print out update and see that we have updated our nested Keyboard POJO this time:

{name='John', favouriteLanguage='Java', keyboard=Keyboard{style='Mechanical', layout='US'}}

The behavior of the annotation is clearly seen here. Incoming fields in nested objects overwrite existing ones. Fields with no match in the new data are left untouched.

5. Merging Maps With @JsonMerge

The process for merging a Map is very similar to what we’ve seen already. Let’s create an object with a Map that we can use to demonstrate:

class ObjectWithMap {
    String name;
    @JsonMerge
    Map<String, String> stringPairs;
    // Standard getters, setters and constructors
}

Following that, let’s create a starter JSON String containing a map we’ll update our object with:

String newData = "{\"stringPairs\":{\"field1\":\"value1\",\"field2\":\"value2\"}}";

Finally, we need the instance of ObjectWithMap we want to update:

Map<String, String> map = new HashMap<>();
map.put("field3", "value3");
ObjectWithMap objectToUpdateWith = new ObjectWithMap("James", map);

We can now use the same process we’ve used before to update our object:

@Test
void givenAnObjectWithAMap_whenUsingJsonMerge_thenExpectAllFieldsInMap() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    ObjectWithMap update = objectMapper.readerForUpdating(objectToUpdateWith).readValue(newData);

    assertTrue(update.getStringPairs().containsKey("field1"));
    assertTrue(update.getStringPairs().containsKey("field2"));
    assertTrue(update.getStringPairs().containsKey("field3"));
}

If we print out update again to get a view of the end result, it looks like this:

{name='James', something={field1=value1, field3=value3, field2=value2}}

We see from the test and the printout that using the annotation has resulted in all three pairs existing in the Map. Without the annotation, we would only have the pairs from the new data.

6. Conclusion

In this article, we’ve seen that we can use Jackson to update an existing object with new incoming JSON data. Furthermore, by using the @JsonMerge annotation in our Java objects, we can get Jackson to merge nested POJOs and Maps. Without the annotation, Jackson will overwrite them, so its usefulness depends on our use case.

As always, the full code for the examples is available 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 – Jackson (eBook) (cat=Jackson)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.