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

Leave a Reply

Your email address will not be published. Required fields are marked *

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