Jackson – Bidirectional Relationships
1. Overview
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
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.