Jackson – Bidirectional Relationships

1. Overview

In this tutorial, we’ll go over the best ways to deal with bidirectional relationships in Jackson.

We’ll discuss the Jackson JSON infinite recursion problem, then – we’ll see how to serialize entities with bidirectional relationships and finally – we will deserialize them.

2. Infinite Recursion

First – let’s take a look at the Jackson infinite recursion problem. In the following example we have two entities – “User” and “Item” – with a simple one-to-many relationship:

The “User” entity:

public class User {
    public int id;
    public String name;
    public List<Item> userItems;
}

The “Item” entity:

public class Item {
    public int id;
    public String itemName;
    public User owner;
}

When we try to serialize an instance of “Item“, Jackson will throw a JsonMappingException exception:

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenSerializing_thenException()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    new ObjectMapper().writeValueAsString(item);
}

The full exception is:

com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError) 
(through reference chain: 
com.maixuanviet.jackson.bidirection.Item["owner"]
->com.maixuanviet.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->com.maixuanviet.jackson.bidirection.Item["owner"]
->…..

Let’s see, over the course of the next few sections – how to solve this problem.

3. Use @JsonManagedReference@JsonBackReference

First, let’s annotate the relationship with @JsonManagedReference@JsonBackReference to allow Jackson to better handle the relation:

Here’s the “User” entity:

public class User {
    public int id;
    public String name;

    @JsonManagedReference
    public List<Item> userItems;
}

And the “Item“:

public class Item {
    public int id;
    public String itemName;

    @JsonBackReference
    public User owner;
}

Let’s now test out the new entities:

@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
    final User user = new User(1, "John");
    final Item item = new Item(2, "book", user);
    user.addItem(item);

    final String itemJson = new ObjectMapper().writeValueAsString(item);
    final String userJson = new ObjectMapper().writeValueAsString(user);

    assertThat(itemJson, containsString("book"));
    assertThat(itemJson, not(containsString("John")));

    assertThat(userJson, containsString("John"));
    assertThat(userJson, containsString("userItems"));
    assertThat(userJson, containsString("book"));
}

Here is the output of serializing the Item object:

{
 "id":2,
 "itemName":"book"
}

And here is the output of serializing the User object:

{
 "id":1,
 "name":"John",
 "userItems":[{
   "id":2,
   "itemName":"book"}]
}

Note that:

  • @JsonManagedReference is the forward part of reference – the one that gets serialized normally.
  • @JsonBackReference is the back part of reference – it will be omitted from serialization.
  • The serialized Item object does not contain a reference to the User object.

Also, note that we cannot switch around the annotations. The following will work for the serialization:

@JsonBackReference
public List<Item> userItems;

@JsonManagedReference
public User owner;

But will throw an exception when we attempt to deserialize the object, as @JsonBackReference cannot be used on a collection.

If we want to have the serialized Item object contain a reference to the User, we need to use @JsonIdentityInfo. We’ll look at that in the next section.

4. Use @JsonIdentityInfo

Now – let’s see how to help with the serialization of entities with bidirectional relationships using @JsonIdentityInfo.

We add the class level annotation to our “User” entity:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class User { ... }

And to the “Item” entity:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class Item { ... }

Time for the test:

@Test
public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, containsString("userItems"));
}

Here is the output of serialization:

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John",
        "userItems":[2]
    }
}

5. Use @JsonIgnore

Alternatively, we can also use the @JsonIgnore annotation to simply ignore one of the sides of the relationship, thus breaking the chain.

In the following example – we will prevent the infinite recursion by ignoring the “User” property “userItems” from serialization:

Here is “User” entity:

public class User {
    public int id;
    public String name;

    @JsonIgnore
    public List<Item> userItems;
}

And here is our test:

@Test
public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, not(containsString("userItems")));
}

And here is the output of serialization:

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John"
    }
}

6. Use @JsonView

We can also use the newer @JsonView annotation to exclude one side of the relationship.

In the following example – we use two JSON Views – Public and Internal where Internal extends Public:

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

We’ll include all User and Item fields in the Public View – except the User field userItems which will be included in the Internal View:

Here is our entity “User“:

public class User {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String name;

    @JsonView(Views.Internal.class)
    public List<Item> userItems;
}

And here is our entity “Item“:

public class Item {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String itemName;

    @JsonView(Views.Public.class)
    public User owner;
}

When we serialize using the Public view, it works correctly – because we excluded userItems from being serialized:

@Test
public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect() 
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writerWithView(Views.Public.class)
      .writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, not(containsString("userItems")));
}

But If we serialize using an Internal view, JsonMappingException is thrown because all the fields are included:

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenUsingInternalJsonView_thenException()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    new ObjectMapper()
      .writerWithView(Views.Internal.class)
      .writeValueAsString(item);
}

7. Use a Custom Serializer

Next – let’s see how to serialize entities with bidirectional relationships using a custom serializer.

In the following example – we will use a custom serializer to serialize the “User” property “userItems“:

Here’s the “User” entity:

public class User {
    public int id;
    public String name;

    @JsonSerialize(using = CustomListSerializer.class)
    public List<Item> userItems;
}

And here is the “CustomListSerializer“:

public class CustomListSerializer extends StdSerializer<List<Item>>{

   public CustomListSerializer() {
        this(null);
    }

    public CustomListSerializer(Class<List> t) {
        super(t);
    }

    @Override
    public void serialize(
      List<Item> items, 
      JsonGenerator generator, 
      SerializerProvider provider) 
      throws IOException, JsonProcessingException {
        
        List<Integer> ids = new ArrayList<>();
        for (Item item : items) {
            ids.add(item.id);
        }
        generator.writeObject(ids);
    }
}

Let’s now test out the serializer and see the right kind of output being produced:

@Test
public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect()
  throws JsonProcessingException {
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, containsString("userItems"));
}

And the final output of the serialization with the custom serializer:

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John",
        "userItems":[2]
    }
}

8. Deserialize With @JsonIdentityInfo

Now – let’s see how to deserialize entities with bidirectional relationships using @JsonIdentityInfo.

Here is the “User” entity:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class User { ... }

And the “Item” entity:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class Item { ... }

Let’s now write a quick test – starting with some manual JSON data we want to parse and finishing with the correctly constructed entity:

@Test
public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect() 
  throws JsonProcessingException, IOException {
    String json = 
      "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

    ItemWithIdentity item
      = new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json);
    
    assertEquals(2, item.id);
    assertEquals("book", item.itemName);
    assertEquals("John", item.owner.name);
}

9. Use Custom Deserializer

Finally, let’s deserialize the entities with a bidirectional relationship using a custom deserializer.

In the following example – we will use a custom deserializer to parse the “User” property “userItems“:

Here’s “User” entity:

public class User {
    public int id;
    public String name;

    @JsonDeserialize(using = CustomListDeserializer.class)
    public List<Item> userItems;
}

And here is our “CustomListDeserializer“:

public class CustomListDeserializer extends StdDeserializer<List<Item>>{

    public CustomListDeserializer() {
        this(null);
    }

    public CustomListDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public List<Item> deserialize(
      JsonParser jsonparser, 
      DeserializationContext context) 
      throws IOException, JsonProcessingException {
        
        return new ArrayList<>();
    }
}

And the simple test:

@Test
public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect()
  throws JsonProcessingException, IOException {
    String json = 
      "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

    Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
 
    assertEquals(2, item.id);
    assertEquals("book", item.itemName);
    assertEquals("John", item.owner.name);
}

10. Conclusion

In this tutorial, we illustrated how to serialize/deserialize entities with bidirectional relationships using Jackson.

The implementation of all these examples and code snippets can be found in our GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Related posts:

Java Program to Implement the Checksum Method for Small String Messages and Detect
Supplier trong Java 8
Debug a HttpURLConnection problem
Java Program to Generate a Random UnDirected Graph for a Given Number of Edges
Disable DNS caching
Java Program to Implement Extended Euclid Algorithm
Jackson – Decide What Fields Get Serialized/Deserialized
Java Program to Find the Number of Ways to Write a Number as the Sum of Numbers Smaller than Itself
Serve Static Resources with Spring
Java Program to Perform Preorder Recursive Traversal of a Given Binary Tree
Recommended Package Structure of a Spring Boot Project
How to Add a Single Element to a Stream
Java Program to Search for an Element in a Binary Search Tree
Join and Split Arrays and Collections in Java
Java Program to Perform Polygon Containment Test
Java Program to Implement Fibonacci Heap
Spring Cloud – Securing Services
Java Program to Check Whether an Input Binary Tree is the Sub Tree of the Binary Tree
Java Program to implement Dynamic Array
Converting between an Array and a List in Java
Java Program to Create a Minimal Set of All Edges Whose Addition will Convert it to a Strongly Conne...
Java Program to Implement Floyd-Warshall Algorithm
HandlerAdapters in Spring MVC
Tạo chương trình Java đầu tiên sử dụng Eclipse
Java Program to Implement K Way Merge Algorithm
Giới thiệu Design Patterns
Java Program to Implement Graph Coloring Algorithm
An Introduction to ThreadLocal in Java
Cơ chế Upcasting và Downcasting trong java
Java Program to Implement Suffix Tree
REST Pagination in Spring
Hướng dẫn Java Design Pattern – DAO