Introduction
Jackson's JSON serializer follows object references recursively, which causes infinite loops and StackOverflowError when serializing entities with bidirectional relationships (parent-child, many-to-many). This is common in JPA entities where both sides of a relationship have references to each other. The serializer enters an endless cycle: parent serializes children, children serialize parent, parent serializes children again. Without intervention, this crashes the application with a stack overflow or produces infinitely nested JSON.
Symptoms
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:766)
at com.example.User$$SpringCGLIB$$...
... (repeated hundreds of times)Or:
java.lang.StackOverflowError
at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:1142)
at com.example.Order.getCustomer(Order.java:45)
at com.example.Customer.getOrders(Customer.java:32)Common Causes
- Bidirectional JPA relationships: @OneToMany with @ManyToOne both serialized
- Self-referencing entities: Category with parent Category reference
- DTO contains entity with circular ref: DTO wraps entity that has circular references
- No Jackson annotations on relationship: Both sides of relationship included in JSON
- Lazy-loaded collection initialized during serialization: Hibernate triggers infinite load
- Generic serialization of entities: Generic endpoint serializes all returned objects
Step-by-Step Fix
Step 1: Use @JsonManagedReference and @JsonBackReference
```java @Entity public class Customer { @Id private Long id; private String name;
@OneToMany(mappedBy = "customer") @JsonManagedReference // Forward reference - serialized private List<Order> orders = new ArrayList<>(); }
@Entity public class Order { @Id private Long id; private BigDecimal total;
@ManyToOne @JoinColumn(name = "customer_id") @JsonBackReference // Back reference - NOT serialized private Customer customer; }
// Output: // {"id":1,"name":"Alice","orders":[{"id":10,"total":99.99}]} // Note: order.customer is NOT included (breaks the cycle) ```
Step 2: Use @JsonIdentityInfo for object references
```java @Entity @JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id" ) public class Category { @Id private Long id; private String name;
@ManyToOne private Category parent;
@OneToMany(mappedBy = "parent") private List<Category> children = new ArrayList<>(); }
// Output: // {"id":1,"name":"Electronics","parent":null,"children":[2,3]} // Children reference their parent by ID, not nested object ```
Step 3: Use DTOs to control serialization
```java // DTO pattern - cleanest approach public record CustomerDTO( Long id, String name, List<OrderSummaryDTO> orders ) { public static CustomerDTO from(Customer customer) { return new CustomerDTO( customer.getId(), customer.getName(), customer.getOrders().stream() .map(OrderSummaryDTO::from) .toList() ); } }
public record OrderSummaryDTO( Long id, BigDecimal total, LocalDate date // No customer reference - no cycle possible ) { public static OrderSummaryDTO from(Order order) { return new OrderSummaryDTO( order.getId(), order.getTotal(), order.getDate() ); } } ```
Prevention
- Use DTOs to serialize data instead of JPA entities
- When entities must be serialized, use @JsonManagedReference/@JsonBackReference for parent-child
- Use @JsonIdentityInfo for self-referencing relationships
- Add @JsonIgnore to the "many" side of bidirectional relationships
- Test serialization of complex entity graphs in integration tests
- Use Jackson's FAIL_ON_EMPTY_BEANS feature to catch serialization issues early
- Monitor for StackOverflowError in production as an indicator of circular serialization