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.StackOverflowErrorduring JSON serializationcom.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
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.Use @JsonIgnore on the back-reference:
- 2.```java
- 3.@Entity
- 4.public class User {
- 5.@Id
- 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.Use @JsonManagedReference and @JsonBackReference:
- 2.```java
- 3.@Entity
- 4.public class User {
- 5.@Id
- 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.Use @JsonIdentityInfo for object identity:
- 2.```java
- 3.@Entity
- 4.@JsonIdentityInfo(
- 5.generator = ObjectIdGenerators.PropertyGenerator.class,
- 6.property = "id"
- 7.)
- 8.public class User {
- 9.@Id
- 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.Use DTOs (recommended approach):
- 2.```java
- 3.// Clean separation between entity and API representation
- 4.public record UserResponse(
- 5.Long id,
- 6.String name,
- 7.List<OrderSummary> orders
- 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
@JsonIgnoreto 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
@JsonViewto 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);
`