- 🔁 Self-referencing @ManyToMany links in Hibernate can easily cause endless JSON serialization loops when you show them in REST APIs.
- 🛑 Adding @JsonIgnore to the other side of the mapping stops StackOverflowExceptions from bidirectional links.
- 💡 Using DTOs is a good way to control how data gets serialized. It also separates your main models from what your API sends back.
- 🔍 Integration tests with MockMvc help make sure your REST endpoints don't create JSON that loops or is badly formed.
- ⚠️ Getting data eagerly and showing entities right from controllers are bad practices. They cause slow performance and serialization problems.
Understanding Hibernate ManyToMany on Same Entity: Avoiding JSON Recursion
Hibernate makes it easier to set up relationships between different parts of your Java applications. But it gets hard when you make a @ManyToMany relationship between entities of the same type. For example, think of Users being friends with other Users, or Employees training other workers. When you show these self-referencing relationships in REST APIs, they often cause endless loops when turning data into JSON. This article explains why these situations cause problems. It also looks at good ways to fix them with Hibernate and Jackson, and shows good ways to make sure REST APIs are safe and work well.
What Is a Self-Referencing ManyToMany Relationship in Hibernate?
Self-referencing @ManyToMany relationships happen when one entity points to others of the same kind. These links are common in how businesses work:
- A user who has friends (and each friend is also a user)
- Employees who train others or are trained by others
- Organizations that work together (for example, as partners)
Hibernate lets you set up these links with its usual annotations.
Here's an example of a one-way mapping:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
private Set<User> friends = new HashSet<>();
}
This lets each user keep a list of other User entities as friends. This works well, until JSON serialization starts. And if you need links that go both ways (for example, if a friend can also see who lists them as a friend), problems grow fast.
The Root of JSON Recursion in Hibernate ManyToMany Relationships
When setting up a hibernate manytomany relationship on the same entity, people often make it go both ways:
@Entity
public class User {
@Id
private Long id;
@ManyToMany
@JoinTable(
name = "user_friends",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "friend_id")
)
private Set<User> friends = new HashSet<>();
@ManyToMany(mappedBy = "friends")
private Set<User> friendOf = new HashSet<>();
}
While this setup shows a link that goes both ways correctly, it creates references that loop back. When a User is turned into JSON, Jackson goes through the friends list for each friend inside it. Then those friends point back to the first user with friendOf, and this continues. This causes:
- 🔁 Endless loops during serialization
- 🧨 A StackOverflowError in your application's logs
- 💥 Crashes in REST APIs that send back all linked data
These problems most often show up in Spring Boot applications that use Jackson to turn data into JSON automatically.
How Jackson Fails: Real-life Example of Recursion
With the model from before, the JSON output might look like this:
{
"id": 1,
"friends": [
{
"id": 2,
"friends": [
{
"id": 1,
"friends": [...]
}
]
}
]
}
The loop never stops because there is nothing in the serialization process itself to stop it. This is why it is very important to break this chain using the right tools.
Solution 1: Breaking Recursion with @JsonIgnore
The simplest fix is to tell the system to ignore one side of the relationship when it turns data into JSON:
@ManyToMany(mappedBy = "friends")
@JsonIgnore
private Set<User> friendOf = new HashSet<>();
Good points:
- ✅ Simple and works
- ✅ Stops StackOverflowError
- ✅ Makes JSON data smaller
Bad points:
- ❌ Hides
friendOffrom JSON completely - ❌ You lose the other side of the data unless you specifically deal with it in your service or business code
Use @JsonIgnore when you only need to show one direction in your API, or when clients do not really need the other side.
Solution 2: Using @JsonManagedReference and @JsonBackReference
Jackson offers a smarter way to fix this with @JsonManagedReference and @JsonBackReference.
You can use them like this:
@ManyToMany
@JsonManagedReference
private Set<User> friends = new HashSet<>();
@ManyToMany(mappedBy = "friends")
@JsonBackReference
private Set<User> friendOf = new HashSet<>();
How this works:
@JsonManagedReferencemeans where the serialization starts@JsonBackReferencemeans it is hidden from the JSON output- Stops loops
This way lets entity relationships go both ways in your database and your application. But it stops loops when turning data into JSON.
Things to consider:
- 🧠 Needs careful matching
- 🔧 It is harder to change or use again for different ways of seeing the same entity
- 📦 Works well for deeply nested object structures if you need them
Solution 3: Applying DTOs to Decouple Entity From Representation
The best practice in the field is to not show JPA entities directly in REST responses. By using Data Transfer Objects (DTOs), you separate your database model from your JSON model.
Example:
public class UserDTO {
private Long id;
private Set<Long> friendIds;
public UserDTO(Long id, Set<Long> friendIds) {
this.id = id;
this.friendIds = friendIds;
}
// Getters and setters
}
How to map:
public UserDTO toDto(User user) {
Set<Long> friendIds = user.getFriends().stream()
.map(User::getId)
.collect(Collectors.toSet());
return new UserDTO(user.getId(), friendIds);
}
This has many good points:
- ✔️ Good control over the JSON output
- ✔️ No loops
- ✔️ Easier to manage API versions and change them
- ✔️ Separate design (a clear split between saving data and turning it into JSON)
You can do even more with this by using libraries like MapStruct or ModelMapper to turn DTOs into other forms automatically.
Bidirectional vs Unidirectional: Which Is Better?
Be smart about how you set up relationships. For hibernate manytomany on the same entity, you might not need links that go both ways.
Unidirectional:
@ManyToMany
private Set<User> friends;
Pros:
- ✅ Simpler database setup and code
- ✅ Stops loops by how it is made
- ✅ Good for JSON serialization
Bad points:
- ❌ Less clear meaning
- ❌ Might need special database queries to find things the other way around
If your application does not need to ask for data the other way around, choose unidirectional. This keeps your API safe from things you did not expect and makes it easier to keep up.
Understanding Hibernate Annotations in Depth
To create good relationships in Hibernate, you need to know how to use its annotations well:
@JoinTable
Used to clearly set up the join table and columns in many-to-many relationships:
@JoinTable(
name = "user_friends",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "friend_id")
)
mappedBy
Shows the “other” or non-owning side of a relationship. Hibernate uses this to figure out which field owns the relationship. It also uses it to handle how changes flow and how data is saved.
Cascading
Using:
@ManyToMany(cascade = CascadeType.ALL)
does the same things (like save, update, delete) to linked entities. Use this with care, especially in same-entity mappings. This will help you avoid deletions or updates you did not mean to make.
Testing for Recursion: Why and How
Set up integration tests to check if your API endpoints work right. Using Spring Boot with MockMvc can help:
@Test
public void whenGetUser_thenNoRecursionOccurs() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.friends").isArray())
.andExpect(jsonPath("$.friends[0].friends").doesNotExist());
}
These tests make sure:
- ✅ JSON data does not loop
- ✅ Your API stays fast
- ✅ Changes do not break old features
Best Practices Recap
To stop hibernate json recursion in self-referencing relationships, do these things:
- ✔️ Use DTOs instead of showing JPA entities
- ✔️ Use one-way links when you can
- ✔️ Use
@JsonIgnore/@JsonBackReferencewisely - ✔️ Watch how deep serialized object structures go
- ✔️ Do not get data eagerly in
@ManyToManyfields - ✔️ Write integration tests for what your API sends back
Keep REST APIs simple and easy to keep up by following these basic rules.
Helpful Tools
- Lombok
@ToString.Exclude: Stops endless loops from printing in logs because of references that loop. - MapStruct or ModelMapper: Turns data between entity and DTO layers automatically, cutting down on repeated code.
- Spring HATEOAS: Adds hypermedia controls and gives you more control over how your JSON data is shaped.
Things to Not Do
- ❌ Sending JPA entities directly from controllers
- ❌ Using
CascadeType.ALLwithout knowing all the effects on linked entities - ❌ Letting deep data fetches happen by default—lazy loading is safer and works better.
- ❌ Thinking DTOs are optional: they are needed for APIs that are well-organized and can be versioned.
Final Thoughts
Setting up self-referencing hibernate manytomany relationships creates problems, especially when shown using JSON in REST APIs. The risk of endless hibernate json loops is real. It can really hurt how stable and fast your application is. Choose the right way to fix it, whether that means using @JsonIgnore, @JsonManagedReference, or making a good DTO system. Go for simplicity, test often, and keep your API clear to understand. With the right practices, your application will be strong, able to grow, and free of serialization problems.
Citations
- FasterXML. (n.d.). Jackson JSON Processor: Infinite Recursion and StackOverflowException. https://github.com/FasterXML/jackson-databind/issues/437
- Red Hat. (2023). Hibernate ORM User Guide. https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#associations-many-to-many
- Baeldung. (2022). Jackson Infinite Recursion and How to Solve It. https://www.baeldung.com/jackson-bidirectional-relationships