Overview
In this blog, we will cover a very common issue that causes the Infinity loop aka (Jackson JSON infinite recursion) when using @ManyToOne and @OneToMany relation between entities and how we can resolve them.
Sample Code being used
@Entity
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID", nullable = false)
private Long id;
@Column(name = "LAST_NAME", nullable = false)
private String lastName;
@OneToMany(fetch = FetchType.LAZY,mappedBy="user")
private List<UserDetail> lstUserDetail;
}
@Entity
@Table(name = "USER_DETAIL")
public class UserDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID", nullable = false)
private Long id;
@Column(name = "PHONE", nullable = false)
private String phone;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "USER_ID")
private User user;
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastName(String lastname);
}
@Autowired
private UserRepository userRepository;
@GetMapping("/user/{username}")
public List<User> getCurrentUser(@PathVariable("username") String username) {
return userRepository.findByLastName(username);
}
Error
ERROR 26260 — [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.example.demo.entity.User[“lstUserDetail”]->org.hibernate.collection.internal.PersistentBag[0]->com.example.demo.entity.UserDetail[“user”]->com.example.demo.entity.User[“lstUserDetail”]->org.hibernate.collection.internal.PersistentBag[0]->com.example.demo.entity.UserDetail[“user”]->com.example.demo.entity.User[“lstUserDetail”]->org.hibernate.collection.internal.PersistentBag[0]->com.example.demo.entity.UserDetail[“user”]->com.example.demo.entity.User[“lstUserDetail”]->org.hibernate.collection.internal.PersistentBag[0]->com.example.demo.entity.UserDetail[“user”]->com.example.demo.entity.User[“lstUserDetail”]->com.example.demo.entity.UserDetail[“user”]->com.example.demo.entity.User[“lstUserDetail”])] with root cause
java.lang.StackOverflowError: null
at java.lang.Exception.<init>(Exception.java:66) ~[na:1.8.0_40]
at java.io.IOException.<init>(IOException.java:58) ~[na:1.8.0_40]
Issue
Since we are using OneToMany and ManyToOne mapping. User => UserDetail => User
When we query User, Hibernate will try to get UserDetail as well. As UserDetail contains a reference to User, it will try to get User and this will go on in loop causing StackOverflowError exception.
Note: Adding fetch=FetchType.LAZY will not work for these scenarios.
Solution
By below way, we will try to break the loop either at User end or at UserDetail.
- Use @JsonManagedReference and @JsonBackReference
- Use @JsonIdentityInfo
- Use @JsonIgnore
@JsonManagedReference and @JsonBackReference
We can mark the entity which we want to have with @JsonManagedReference and the one which we don’t want in JSON with @JsonBackReference. Something like this
/* In User Entity */
@OneToMany(fetch = FetchType.LAZY,mappedBy="user")
@JsonManagedReference
private List<UserDetail> lstUserDetail;
/* In UserDetail Entity */
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "USER_ID")
@JsonBackReference
private User user;
With the above changes, JSON will be printed as follows.
[
{
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com",
"lstUserDetail": [
{
"id": 1,
"phone": "123456"
},
{
"id": 2,
"phone": "123453"
},
{
"id": 3,
"phone": "123453"
},
{
"id": 4,
"phone": "234234"
},
{
"id": 5,
"phone": "3554534"
}
]
}
]
Notice that lstUserDetail is not having User reference thus breaking the loop.
We can do other way around as well
@OneToMany(fetch = FetchType.LAZY,mappedBy="user")
@JsonBackReference
private List<UserDetail> lstUserDetail;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "USER_ID")
@JsonManagedReference
private User user;
/* Fetch User */
[
{
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
]
/* Fetch UserDetail */
[
{
"id": 1,
"phone": "123456",
"user": {
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
},
{
"id": 2,
"phone": "123453",
"user": {
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
},
{
"id": 3,
"phone": "123453",
"user": {
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
},
{
"id": 4,
"phone": "234234",
"user": {
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
}
]
@JsonIdentityInfo
By adding @JsonIdentityInfo we are breaking the loop by just providing the id field of referring Entity. Notice user field in UserDetail JSON.
@Entity
@Table(name = "USERS")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User {
...
}
@Entity
@Table(name = "USER_DETAIL")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class UserDetail {
...
}
/* JSON when we fetch User */
[
{
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com",
"lstUserDetail": [
{
"id": 1,
"phone": "123456",
"user": 1
},
{
"id": 2,
"phone": "123453",
"user": 1
},
{
"id": 3,
"phone": "123453",
"user": 1
},
{
"id": 4,
"phone": "234234",
"user": 1
}
]
}
]
@JsonIgnore
With @JsonIgnore we are breaking loop by ignoring the referring entity.
For Example if we want to ignore UserDetail from the User entity
@OneToMany(fetch = FetchType.LAZY,mappedBy="user")
@JsonIgnore
private List<UserDetail> lstUserDetail;
/* JSON when we fetch User */
[
{
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com"
}
]
Similarly, we can also ignore User From UserDetail entity
/* In UserDetail Entity */
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "USER_ID")
@JsonIgnore
private User user;
/* JSON when we fetch User */
[
{
"id": 1,
"lastName": "test0",
"firstName": "data0",
"email": "sampleEmail0@test.com",
"lstUserDetail": [
{
"id": 1,
"phone": "123456"
},
{
"id": 2,
"phone": "123453"
},
{
"id": 3,
"phone": "123453"
},
{
"id": 4,
"phone": "234234"
}
}
]
Demo
You can see a live demo of all the above-mentioned implementation here.
0 Comments