Question 25 · Section 16

How to Avoid Infinite Recursion When Serializing Entities

When serializing bidirectional relationships (e.g., to JSON for REST API), infinite recursion occurs, leading to StackOverflowError. There are several approaches to solving this...

Language versions: English Russian Ukrainian

Overview

When serializing bidirectional relationships (e.g., to JSON for REST API), infinite recursion occurs, leading to StackOverflowError. There are several approaches to solving this problem.


Junior Level

The Problem

When serializing bidirectional relationships, infinite recursion occurs:

@Entity
public class Order {
    @ManyToOne
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

// JSON serialization:
// Order -> User -> Orders -> User -> Orders -> ... -> StackOverflowError

Solutions

  1. @JsonIgnore - ignore the field
  2. @JsonManagedReference / @JsonBackReference - break the cycle
  3. DTO projection - use DTO instead of entity

These are Jackson-specific annotations (not JPA!). They only work when serializing via Jackson in Spring Boot REST controllers.

// Solution 1: @JsonIgnore
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonIgnore  // don't serialize
    private List<Order> orders;
}

// Solution 2: Managed/BackReference
@Entity
public class Order {
    @ManyToOne
    @JsonBackReference  // don't serialize
    private User user;
}

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonManagedReference  // serialize
    private List<Order> orders;
}

Middle Level

Detailed Solutions

@JsonIgnore

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonIgnore  // completely ignore during serialization
    private List<Order> orders;
}

// GET /users/1
// { "id": 1, "name": "John" }  // without orders

@JsonManagedReference / @JsonBackReference

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonManagedReference  // serialize this side
    private List<Order> orders;
}

@Entity
public class Order {
    @ManyToOne
    @JsonBackReference  // DON'T serialize this side
    private User user;
}

// GET /users/1
// { "id": 1, "name": "John", "orders": [
//     { "id": 1, "status": "new" },  // user not included
//     { "id": 2, "status": " shipped" }
// ]}

DTO (Best Solution)

// DTO - full control over serialization
public record OrderDto(
    Long id,
    String status,
    String userName,
    LocalDateTime createdAt
) {}

// Mapping
@Query("""
    SELECT new com.example.OrderDto(
        o.id, o.status, u.name, o.createdAt
    )
    FROM Order o JOIN o.user u
    WHERE o.id = :id
    """)
OrderDto findOrderDto(@Param("id") Long id);

// GET /orders/1
// { "id": 1, "status": "new", "userName": "John", "createdAt": "..." }

Common Mistakes

// Serializing entity directly
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        return service.getOrder(id);  // StackOverflowError
    }
}

// DTO instead of entity
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
    return service.getOrderDto(id);  // correct
}

Senior Level

@JsonIdentityInfo

@Entity
@JsonIdentityInfo(
    generator = ObjectIdGenerators.PropertyGenerator.class,
    property = "id"
)
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

// JSON: User with ID, Orders reference User ID
// {
//     "id": 1,
//     "name": "John",
//     "orders": [
//         { "id": 1, "status": "new", "user": 1 },
//         { "id": 2, "status": "shipped", "user": 1 }
//     ]
// }
// Mechanism: first occurrence of object serialized fully (with "@id": 1).
// Subsequent references - only ID: {"@id": 1}. This prevents infinite loops.

Architectural Approach

Rule: Never serialize JPA entities directly.

Why:
1. Infinite recursion
2. Lazy field loading outside transaction
3. Exposing internal DB structure
4. Impossible API contract control
5. Performance problems

**LazyInitializationException during serialization:** when REST controller returns entity,
Jackson serializer tries to access LAZY field outside transaction -> exception.
This is one of the most common production issues.

Solution: DTO pattern

DTO Mapping

// Pattern 1: Record constructor in JPQL
@Query("""
    SELECT new com.example.UserDto(
        u.id, u.name, u.email,
        (SELECT COUNT(o) FROM Order o WHERE o.user.id = u.id)
    )
    FROM User u
    """)
List<UserDto> findAllUsers();

// Pattern 2: MapStruct
@Mapper(componentModel = "spring")
public interface OrderMapper {
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "items", target = "items")
    OrderDto toDto(Order order);
}

// Pattern 3: Method in entity
@Entity
public class Order {
    public OrderDto toDto() {
        return new OrderDto(id, status, user.getName(), createdAt);
    }
}

Best Practices

DTO projection (best solution)
@JsonManagedReference/BackReference (quick solution)
@JsonIgnore for one side
@JsonIdentityInfo for cyclic references
Never serialize entities directly

Without annotations (StackOverflowError)
EAGER + serialization
Direct entity return from REST controller
Ignoring lazy loading during serialization

Interview Cheat Sheet

Must know:

  • Bidirectional relationships -> infinite recursion on JSON serialization -> StackOverflowError
  • 4 solutions: @JsonIgnore, @JsonManagedReference/@JsonBackReference, @JsonIdentityInfo, DTO projection
  • DTO projection is the best solution: full API control, no lazy issues, contract isolation
  • @JsonManagedReference serializes parent -> child, @JsonBackReference skips child -> parent
  • Never return entities from REST controller directly
  • LazyInitializationException during serialization - Jackson accesses LAZY outside @Transactional

Frequent follow-up questions:

  • Why is DTO better than annotations? API contract isolation, no lazy issues, field control, performance
  • How does @JsonIdentityInfo work? First occurrence - full object with “@id”, subsequent - only ID
  • What is LazyInitializationException during serialization? Jackson tries to read LAZY field outside @Transactional
  • Why MapStruct? Automatic entity -> DTO mapping, less boilerplate code

Red flags (DO NOT say):

  • “I return entities from REST controller” - StackOverflowError + LazyInitializationException
  • “EAGER to avoid serialization problems” - creates N+1, doesn’t solve root cause
  • “Without annotations - hoping for the best” - StackOverflowError guaranteed
  • “I serialize entities directly for simplicity” - technical debt, exposes DB structure

Related topics:

  • [[24. What Are the Peculiarities of Bidirectional Relationships]]
  • [[29. What is Projection in JPA]]
  • [[4. What is LazyInitializationException and How to Avoid It]]
  • [[23. How to Properly Use @OneToMany and @ManyToOne]]