Jackson Bidirectional Relationships
In this example, we will go through the Infinite Recursion (StackOverflowError) problem, when working with Bidirectional Relationships in Jackson model classes. We will also see different techniques to serialize and deserialize such entities.
If you are new to Jackson, it is good to go through this primer on ObjectMapper before proceeding with this example.
1. What is a Bidirectional Relationship?
Let’s have a look at a simple bi-directional relationship in Jackson entities. This section shows two entities Province and City. There exists a one-to-many relationship between Province and City, and a one-to-one relationship between City and Province.
Province.java
public class Province { public int id; public String name; public List<Cities> cities = new ArrayList(); public Province(int id, String name) { this.id = id; this.name = name; } public void addCity(City city) { cities.add(city); } }
City.java
public class City { public int id; public String name; public Province province; public City(int id, String name, Province province) { this.id = id; this.name = name; this.province = province; } }
1.1. Infinite Recursion Problem
When we attempt to serialize an instance of either of the above two entities, a JsonMappingException exception is thrown.
Main Method
public static void main(String[] args) { try { Province north = new Province(1, "North-Province"); City city = new City(110006, "Delhi", north); City city2 = new City(160003, "Chandigarh", north); north.addCity(city); north.addCity(city2); System.out.println(new ObjectMapper() .writerWithDefaultPrettyPrinter().writeValueAsString(north)); } catch (JsonProcessingException ex) { System.out.println(ex.getClass().getName() + " : " + ex.getMessage()); } }
The complete exception and stack trace is:
Stack Trace
com.fasterxml.jackson.databind.JsonMappingException : Infinite recursion (StackOverflowError) (through reference chain: Province["cities"] -> java.util.ArrayList[0] -> City["province"] -> Province["cities"]
As we can see above, the Jackson API is unable to serialize the entities due to the presence of infinite recursion. When these entities are attempted for serialization, the presence of a bi-directional relationship results in a cycle. This causes the serialization to fail.
Let’s see in the following sections – how to deal with infinite recursion problem in such entities.
2. Using @JsonManagedReference and @JsonBackReference
The annotation @JsonManagedReference
is used to mark a field as a “forward” link in a bi-directional linkage. This field is serialized normally. However, the type of this field should contain a compatible property that must be annotated with @JsonBackReference
. This property is usually referred to as the “child” or the “back” link and is ignored for serialization.
Following are the new entities with these annotations.
2.1. @JsonManagedReference – “One To Many” End
Province.java [Fix 2.1]
public class Province { public int id; public String name; @JsonManagedReference public List<City> cities = new ArrayList(); public void addCity(City city) { cities.add(city); } }
City.java [Fix 2.1]
public class City { public int id; public String name; @JsonBackReference public Province province; }
Main Method [Fix 2.1]
public static void main(String[] args) throws JsonProcessingException { Province north = new Province(1, "North-Province"); City city = new City(110006, "Delhi", north); City city2 = new City(160003, "Chandigarh", north); north.addCity(city); north.addCity(city2); System.out.println(new ObjectMapper(). writerWithDefaultPrettyPrinter().writeValueAsString(north)); }
Output [Fix 2.1]
{ "id" : 1, "name" : "North-Province", "cities" : [ { "id" : 110006, "name" : "Delhi" }, { "id" : 160003, "name" : "Chandigarh" } ] }
The output above clearly shows that the province field in the City entity is skipped for serialization as it is annotated with @JsonBackReference
2.2. @JsonManagedReference – “One To One” End
Province.java [Fix 2.2]
public class Province { public int id; public String name; @JsonBackReference public List<City> cities = new ArrayList(); public void addCity(City city) { cities.add(city); } }
City.java [Fix 2.2]
public class City { public int id; public String name; @JsonManagedReference public Province province; }
Output [Fix 2.2]
{ "id" : 1, "name" : "North-Province" }
As seen in the above output, the cities field in the Province class is skipped for serialization as it annotated with @JsonBackReference
3. Using @JsonIdentityInfo
The @JsonIdentityInfo
annotation is another solution when dealing with bi-directional relationships. The following example demonstrates the use of this annotation that breaks the cycle of infinite recursion.
3.1. Serialization
Province.java [With Fix 3.1]
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Province { ......... }
City.java [With Fix 3.1]
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class City { .......... }
Main Method [With Fix 3.1]
public static void main(String[] args) throws JsonProcessingException { System.out.println(mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(city) + " \n"); System.out.println(mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(north)); }
Output [With Fix 3.1]
{ "id" : 110006, "name" : "Delhi", "province" : { "id" : 1, "name" : "North-Province", "cities" : [ 110006, { "id" : 160003, "name" : "Chandigarh", "province" : 1 } ] } } { "id" : 1, "name" : "North-Province", "cities" : [ { "id" : 110006, "name" : "Delhi", "province" : 1 }, { "id" : 160003, "name" : "Chandigarh", "province" : 1 } ] }
It is clear from the above output that whenever an instance of a POJO appears for a second time for serialization, it is replaced by the id property in the JSON. This is because we annotated the entity classes with @JsonIdentityInfo
and defined “id” to be used as a PropertyGenerator.
3.2. Deserialization
Deserialization [With Fix 3.2]
private static void deserialzeCircularRelations(ObjectMapper mapper) throws JsonProcessingException, JsonMappingException { String cityString = "{\"id\":110006,\"name\":\"Delhi\"," + "\"province\":{\"id\":1,\"name\":\"North-Province\"," + "\"cities\":[110006,{\"id\":160003,\"name\":\"Chandigarh\"" + ",\"province\":1}]}}"; City cityObj = mapper.readValue(cityString, City.class); System.out.println(cityObj); // prints // City [id=110006, name=Delhi, province=Province // [id=1, name=North-Province]] }
Likewise, when deserializing a JSON string, these annotations help ObjectMapper to construct the entity correctly.
For a deeper dive on other Jackson annotations, you can find an example here.
4. Using Custom Serializer and Deserializer
Finally, let’s create our custom serializer and deserializer to work with the entities having bidirectional relationships.
4.1. Custom Serializer
Province.java
public class Province { .............. @JsonSerialize(using = CustomBiDirectionalSerializer.class) public List<City> cities = new ArrayList(); .............. }
CustomBiDirectionalSerializer.java
public class CustomBiDirectionalSerializer extends StdSerializer<List> { public CustomBiDirectionalSerializer() { this(null); } public CustomBiDirectionalSerializer(Class<List> clazz) { super(clazz); } @Override public void serialize(List<City> cities, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { List<Integer> cityIds = new ArrayList(); for (City city : cities) { cityIds.add(city.id); } jsonGenerator.writeObject(cityIds); } }
The above custom serializer processes the List<City> object by serializing it as a List of city.id and ignores the name and the Province fields.
Main Method [Custom Serializer]
public static void main(String[] args) throws JsonProcessingException { System.out.println(new ObjectMapper() .writerWithDefaultPrettyPrinter().writeValueAsString(city2)); }
Output [Custom Serializer]
{ "id" : 160003, "name" : "Chandigarh", "province" : { "id" : 1, "name" : "North-Province", "cities" : [ 110006, 160003 ] } }
4.2. Custom Deserializer
Province.java
public class Province { .......... @JsonDeserialize(using = CustomBiDirectionalDeSerializer.class) public List<City> cities = new ArrayList(); .......... }
CustomBiDirectionalDeSerializer
public class CustomBiDirectionalDeSerializer extends StdDeserializer<List> { public CustomBiDirectionalDeSerializer() { this(null); } public CustomBiDirectionalDeSerializer(Class<List> clazz) { super(clazz); } @Override public List<City> deserialize(JsonParser jsonParser, DeserializationContext deSerContext) throws IOException, JsonProcessingException { return new ArrayList(); } }
The above custom deserializer works by returning an empty list of type City on encountering a JSON array for deserialization.
Main Method [Custom Deserializer]
public static void main(String[] args) throws IOException { String deSerString = "{\"id\":110006,\"name\":\"Delhi\"," + "\"province\":{\"id\":1,\"name\":\"North-Province\"," + "\"cities\":[110006,{\"id\":160003,\"name\":\"Chandigarh\"" + ",\"province\":1}]}}"; City cityObj = new ObjectMapper().readValue(deSerString, City.class); System.out.println(cityObj); // Print City [id=110006, name=Delhi, // province=Province [id=1, name=North-Province]] }
5. Conclusion
In this article, we learned about the Bidirectional relationships in Jackson entities and the Infinite Recursion Problem. We also demonstrated the different programming strategies to serialize/deserialize such entities.
6. Download the source code
That was an article about Jackson Bidirectional Relationships.
You can download the full source code of this example here: Jackson Bidirectional Relationships