Introduction

JPA entities with bidirectional relationships (e.g., Parent has many Children, each Child references Parent) create a circular reference graph. When Jackson serializes such entities, it follows the references infinitely: Parent -> Child -> Parent -> Child... until the stack overflows. This is the most common serialization error in Spring Boot REST APIs using JPA entities.

Symptoms

  • java.lang.StackOverflowError during JSON serialization
  • com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion
  • Response never completes - connection hangs until timeout
  • Stack trace shows repeating pattern: User.getOrders() -> Order.getUser() -> User.getOrders()
  • Works for entities without bidirectional relationships
bash
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: Infinite recursion (StackOverflowError)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:776)
Caused by: java.lang.StackOverflowError
    at com.example.User.getOrders(User.java:35)
    at com.example.Order.getUser(Order.java:28)
    at com.example.User.getOrders(User.java:35)
    at com.example.Order.getUser(Order.java:28)
    ... (repeating thousands of times)

Common Causes

  • Bidirectional @OneToMany/@ManyToOne relationships
  • @ManyToMany relationships in both directions
  • Parent-child self-referencing entities
  • Serializing JPA entities directly as REST responses
  • Missing Jackson annotations on bidirectional relationships

Step-by-Step Fix

  1. 1.Use @JsonIgnore on the back-reference:
  2. 2.```java
  3. 3.@Entity
  4. 4.public class User {
  5. 5.@Id
  6. 6.private Long id;

@OneToMany(mappedBy = "user") private List<Order> orders; // This side serializes orders }

@Entity public class Order { @Id private Long id;

@ManyToOne @JsonIgnore // Break the cycle - don't serialize user back private User user; } // Serialization: User -> [Order, Order, ...] - stops here ```

  1. 1.Use @JsonManagedReference and @JsonBackReference:
  2. 2.```java
  3. 3.@Entity
  4. 4.public class User {
  5. 5.@Id
  6. 6.private Long id;

@OneToMany(mappedBy = "user") @JsonManagedReference // "Forward" side - gets serialized private List<Order> orders; }

@Entity public class Order { @Id private Long id;

@ManyToOne @JsonBackReference // "Back" side - not serialized, but deserialized private User user; } // JSON: {"id": 1, "orders": [{"id": 10}, {"id": 11}]} // Order.user is not in JSON but is restored during deserialization ```

  1. 1.Use @JsonIdentityInfo for object identity:
  2. 2.```java
  3. 3.@Entity
  4. 4.@JsonIdentityInfo(
  5. 5.generator = ObjectIdGenerators.PropertyGenerator.class,
  6. 6.property = "id"
  7. 7.)
  8. 8.public class User {
  9. 9.@Id
  10. 10.private Long id;

@OneToMany(mappedBy = "user") private List<Order> orders; }

// JSON includes reference ID instead of full object // {"id": 1, "orders": [{"id": 10, "user": 1}, {"id": 11, "user": 1}]} ```

  1. 1.Use DTOs (recommended approach):
  2. 2.```java
  3. 3.// Clean separation between entity and API representation
  4. 4.public record UserResponse(
  5. 5.Long id,
  6. 6.String name,
  7. 7.List<OrderSummary> orders
  8. 8.) {}

public record OrderSummary( Long id, LocalDate date, BigDecimal total ) {}

// In controller @GetMapping("/users/{id}") public UserResponse getUser(@PathVariable Long id) { User user = userService.findById(id); return new UserResponse( user.getId(), user.getName(), user.getOrders().stream() .map(o -> new OrderSummary(o.getId(), o.getDate(), o.getTotal())) .toList() ); } ```

Prevention

  • Never serialize JPA entities directly in REST controllers
  • Always use DTOs or response records for API responses
  • Add @JsonIgnore to back-references in entity classes
  • Use MapStruct to automate entity-to-DTO conversion
  • Add integration tests that verify JSON serialization of all API responses
  • Use @JsonView to control which fields are serialized per endpoint
  • Configure Jackson to fail on circular references instead of stack overflow:
  • ```java
  • mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, true);
  • `