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:
org.baeldung.jackson.bidirection.Item["owner"]
->org.baeldung.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.baeldung.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;

    @JsonBackReference
    public List<Item> userItems;
}

And the “Item“:

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

    @JsonManagedReference
    public User owner;
}

Let’s now test out the new entities:

@Test
public void
  givenBidirectionRelation_whenUsingJacksonReferenceAnnotation_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")));
}

Here is the output of serialization:

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

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.

4. Use @JsonIdentityInfo

Now – let’s see how to help with the serialization of entities with bidirectional relationship 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 relationship 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 relationship 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 bidirectional relationship using a custom deserializer.

In the following example – we will use 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.

Leave a Reply

Your email address will not be published.