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

bash
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:

bash
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