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 Perform Matrix Multiplication
Shuffling Collections In Java
An Introduction to Java.util.Hashtable Class
Java – Combine Multiple Collections
A Guide to JPA with Spring
Java Program to Find Hamiltonian Cycle in an UnWeighted Graph
Concurrent Test Execution in Spring 5
Jackson – Decide What Fields Get Serialized/Deserialized
Hướng dẫn sử dụng Java Annotation
Serve Static Resources with Spring
Java Timer
Java Program to Implement the linear congruential generator for Pseudo Random Number Generation
Injecting Prototype Beans into a Singleton Instance in Spring
So sánh HashMap và Hashtable trong Java
Java Program to Implement ScapeGoat Tree
Java Program to Generate a Random UnDirected Graph for a Given Number of Edges
Java Program to Implement Efficient O(log n) Fibonacci generator
REST Web service: Upload và Download file với Jersey 2.x
Hướng dẫn Java Design Pattern – Flyweight
Java Program to Implement Find all Back Edges in a Graph
Java Program to implement Priority Queue
Lập trình đa luồng với CompletableFuture trong Java 8
Từ khóa static và final trong java
Default Password Encoder in Spring Security 5
Java Program to Perform Postorder Recursive Traversal of a Given Binary Tree
Java CyclicBarrier vs CountDownLatch
Java Program to Use Above Below Primitive to Test Whether Two Lines Intersect
Hướng dẫn Java Design Pattern – Mediator
Java – Try with Resources
Spring RestTemplate Request/Response Logging
Guide to the Volatile Keyword in Java
Show Hibernate/JPA SQL Statements from Spring Boot