Saturday 1 February 2020

Jackson: How to handle bidirectional relationships?


In this post, I am going to explain how the bidirectional relationships are handled in Jackson.

Let’s take an example of simple one-to-many relationship (User-to-DeliveredItem).



Model classes looks like below.
public class User {
	private int id;
	private String firstName;
	private String lastName;

	List<DeliveredItem> deliveredItems;

	.....
	.....
	.....
}

public class DeliveredItem {
	private int itemId;
	private String itemName;

	private User deliveredTo;

	.....
	.....
	.....
}


When you try to Serialize User entity, you will end up in StackOverflowError.
User user1 = new User();
		
user1.setId(1);
user1.setFirstName("Ram");
user1.setLastName("Gurram");
		
DeliveredItem item1 = new DeliveredItem();
item1.setDeliveredTo(user1);
item1.setItemId(1);
item1.setItemName("Sofa Set");
		
user1.setDeliveredItems(new ArrayList () {
	{
		add(item1);
	}
});
		
ObjectMapper objectMapper = new ObjectMapper();
		
String json = objectMapper.writeValueAsString(user1);

How to resolve StackOverflowError?
Solution 1: Using @JsonManagedReference, @JsonBackReference annotations, we can get resolve these errors.

@JsonManagedReference
It represents the forward part of reference. If you annotate any bidirectional linking field with this annotation, then the field gets serialized normally.

@JsonBackReference

It represents the back part of reference. If you annotate any bidirectional linking field with this annotation, then it will be omitted from serialization.
public class User {
	private int id;
	private String firstName;
	private String lastName;

	@JsonManagedReference
	List<DeliveredItem> deliveredItems;

	.....
	.....
}

public class DeliveredItem {
	private int itemId;
	private String itemName;

	@JsonBackReference
	private User deliveredTo;

	.....
	.....
}

Find the below working application.

DeliveredItem.java
package com.sample.app.model;

import com.fasterxml.jackson.annotation.JsonBackReference;

public class DeliveredItem {
	private int itemId;
	private String itemName;

	@JsonBackReference
	private User deliveredTo;

	public int getItemId() {
		return itemId;
	}

	public void setItemId(int itemId) {
		this.itemId = itemId;
	}

	public String getItemName() {
		return itemName;
	}

	public void setItemName(String itemName) {
		this.itemName = itemName;
	}

	public User getDeliveredTo() {
		return deliveredTo;
	}

	public void setDeliveredTo(User deliveredTo) {
		this.deliveredTo = deliveredTo;
	}

}


User.java
package com.sample.app.model;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;

public class User {
	private int id;
	private String firstName;
	private String lastName;

	@JsonManagedReference
	List<DeliveredItem> deliveredItems;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public List<DeliveredItem> getDeliveredItems() {
		return deliveredItems;
	}

	public void setDeliveredItems(List<DeliveredItem> deliveredItems) {
		this.deliveredItems = deliveredItems;
	}

}


App.java
package com.sample.app;

import java.util.ArrayList;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sample.app.model.DeliveredItem;
import com.sample.app.model.User;

public class App {
	public static void main(String args[]) throws JsonProcessingException {
		User user1 = new User();
		
		user1.setId(1);
		user1.setFirstName("Ram");
		user1.setLastName("Gurram");
		
		DeliveredItem item1 = new DeliveredItem();
		item1.setDeliveredTo(user1);
		item1.setItemId(1);
		item1.setItemName("Sofa Set");
		
		user1.setDeliveredItems(new ArrayList () {
			{
				add(item1);
			}
		});
		
		ObjectMapper objectMapper = new ObjectMapper();
		
		String json = objectMapper.writeValueAsString(user1);
		
		System.out.println(json);
		
	}
}

Output
{"id":1,"firstName":"Ram","lastName":"Gurram","deliveredItems":[{"itemId":1,"itemName":"Sofa Set"}]}

Solution 2: Using @JsonIdentityInfo
@JsonIdentityInfo is used to solve circular reference of an object by serializing the backreference identifier rather than fully serializing the object.


@JsonIdentityInfo serialize the POJO by it's id when it is encountered second time during serialization.
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "itemId")
public class DeliveredItem {
	private int itemId;
	private String itemName;

	private User deliveredTo;

	.....
	.....
}

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class User {
	private int id;
	private String firstName;
	private String lastName;

	List<DeliveredItem> deliveredItems;

	.....
	.....
}

Find the below working application.

DeliveredItem.java
package com.sample.app.model;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "itemId")
public class DeliveredItem {
	private int itemId;
	private String itemName;

	private User deliveredTo;

	public int getItemId() {
		return itemId;
	}

	public void setItemId(int itemId) {
		this.itemId = itemId;
	}

	public String getItemName() {
		return itemName;
	}

	public void setItemName(String itemName) {
		this.itemName = itemName;
	}

	public User getDeliveredTo() {
		return deliveredTo;
	}

	public void setDeliveredTo(User deliveredTo) {
		this.deliveredTo = deliveredTo;
	}

}


User.java
package com.sample.app.model;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class User {
	private int id;
	private String firstName;
	private String lastName;

	List<DeliveredItem> deliveredItems;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public List<DeliveredItem> getDeliveredItems() {
		return deliveredItems;
	}

	public void setDeliveredItems(List<DeliveredItem> deliveredItems) {
		this.deliveredItems = deliveredItems;
	}

}

When you ran App.java, you will get below messages in console.

{"id":1,"firstName":"Ram","lastName":"Gurram","deliveredItems":[{"itemId":1,"itemName":"Sofa Set","deliveredTo":1}]}

From the output, you can confirm that the id of user is taken in serialization.

Solution 3: Using @JsonIgnore

Just ignore one side of circular relation.
public class DeliveredItem {
	private int itemId;
	private String itemName;

	@JsonIgnore
	private User deliveredTo;

	.....
	.....
}

Solution 4: Using @JsonView
Use JsonViews to get more control on serialization and deserialization.


Views.java
package com.sample.app.model;

public class Views {
	public static class Public {
	}

	public static class Internal extends Public {
	}
}


Apply the Views on DeliveredItem class.
public class DeliveredItem {

	@JsonView(Views.Public.class)
	private int itemId;
	@JsonView(Views.Public.class)
	private String itemName;

	@JsonView(Views.Internal.class)
	private User deliveredTo;

	.....
	.....
}

Use Views while serializing the data.

String json = objectMapper.writerWithView(Views.Public.class).writeValueAsString(user1);

Find the below working application.


DeliveredItem.java
package com.sample.app.model;

import com.fasterxml.jackson.annotation.JsonView;

public class DeliveredItem {

	@JsonView(Views.Public.class)
	private int itemId;
	@JsonView(Views.Public.class)
	private String itemName;

	@JsonView(Views.Internal.class)
	private User deliveredTo;

	public int getItemId() {
		return itemId;
	}

	public void setItemId(int itemId) {
		this.itemId = itemId;
	}

	public String getItemName() {
		return itemName;
	}

	public void setItemName(String itemName) {
		this.itemName = itemName;
	}

	public User getDeliveredTo() {
		return deliveredTo;
	}

	public void setDeliveredTo(User deliveredTo) {
		this.deliveredTo = deliveredTo;
	}

}


App.java
package com.sample.app;

import java.util.ArrayList;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sample.app.model.DeliveredItem;
import com.sample.app.model.User;
import com.sample.app.model.Views;

public class App {
	public static void main(String args[]) throws JsonProcessingException {
		User user1 = new User();
		
		user1.setId(1);
		user1.setFirstName("Ram");
		user1.setLastName("Gurram");
		
		DeliveredItem item1 = new DeliveredItem();
		item1.setDeliveredTo(user1);
		item1.setItemId(1);
		item1.setItemName("Sofa Set");
		
		user1.setDeliveredItems(new ArrayList () {
			{
				add(item1);
			}
		});
		
		ObjectMapper objectMapper = new ObjectMapper();
		
		String json = objectMapper.writerWithView(Views.Public.class).writeValueAsString(user1);
		
		System.out.println(json);
		
	}
}

Run App.java, you will see below messages in console.

{"id":1,"firstName":"Ram","lastName":"Gurram","deliveredItems":[{"itemId":1,"itemName":"Sofa Set"}]}

Solution 5: Use Custom Serializer and Deserializers. 
https://self-learning-java-tutorial.blogspot.com/2020/01/jackson-custom-serializer.html

https://self-learning-java-tutorial.blogspot.com/2020/01/jackson-jsonserialize-custom.html




Previous                                                    Next                                                    Home

No comments:

Post a Comment