Jackson

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 @JsonManagedReferenceis 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.

Download
You can download the full source code of this example here: Jackson Bidirectional Relationships

Anmol Deep

Anmol Deep is a senior engineer currently working with a leading identity security company as a Web Developer. He has 8 years of programming experience in Java and related technologies (including functional programming and lambdas) , Python, SpringBoot, Restful architectures, shell scripts, and databases relational(MySQL, H2) and nosql solutions (OrientDB and MongoDB). He is passionate about researching all aspects of software development including technology, design patterns, automation, best practices, methodologies and tools, and love traveling and photography when not coding.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button