Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Hibernate ManyToMany: How to Avoid Recursion?

Struggling with recursion in Hibernate ManyToMany on the same entity? Learn how to solve it using annotations like @JsonIgnore.
Java developer facing infinite recursion issue with Hibernate @ManyToMany mapping, surrounded by error icons and loops Java developer facing infinite recursion issue with Hibernate @ManyToMany mapping, surrounded by error icons and loops
  • 🔁 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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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 friendOf from 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:

  • @JsonManagedReference means where the serialization starts
  • @JsonBackReference means 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 / @JsonBackReference wisely
  • ✔️ Watch how deep serialized object structures go
  • ✔️ Do not get data eagerly in @ManyToMany fields
  • ✔️ 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.ALL without 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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading