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.


1 Comment

Lupita GarEs · June 15, 2023 at 5:19 am

Excellent, your explanation about the cause of the error helped me, that gave me a clue to solve this error that was happening to me although in my case the relationship was with the same table and the only thing that helped was the use of @JsonIgnore. But reading your explanation gave me clarity šŸ˜€

Thank you! And regards.

Leave a Reply

Avatar placeholder

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