How to Avoid Infinite Recursion When Serializing Entities
Returning a JPA entity with a bidirectional relationship directly from a Spring Boot @RestController causes the JSON serializer (by default, Jackson) to traverse reciprocal obje...
Returning a JPA entity with a bidirectional relationship directly from a Spring Boot @RestController causes the JSON serializer (by default, Jackson) to traverse reciprocal object pointers in an infinite circular loop: User references Order, Order references User, and so on. This unrecoverably exhausts the call stack, terminating the request thread with a java.lang.StackOverflowError.
🟢 Junior Level
Mechanics of the Jackson Serialization Loop
Jackson operates via Java Reflection, inspecting and invoking every JavaBean getter on the target object:
@Entity
public class User {
@Id
private Long id;
@OneToMany(mappedBy = "user")
private List<Order> orders; // getUser() returns Order list
}
@Entity
public class Order {
@Id
private Long id;
@ManyToOne
private User user; // getUser() returns User object
}
JACKSON CIRCULAR RECURSION TRAVERSAL
User { id: 1 }
└── orders: [ Order { id: 10 } ]
└── user: User { id: 1 }
└── orders: [ Order { id: 10 } ]
└── user: ... ──► 💥 StackOverflowError
3 Quick Annotation Fixes in Jackson
1. @JsonIgnore
Suppresses the target property from serialization completely, omitting it from the resulting JSON payload:
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonIgnore // Suppresses the 'user' field from the Order JSON output
private User user;
}
2. @JsonManagedReference and @JsonBackReference
Jackson’s specialized paired annotations for bidirectional associations:
@JsonManagedReference(on the parent entity): Serialized normally into the JSON output.@JsonBackReference(on the child entity): Omitted during serialization, breaking the infinite cycle.
@Entity
public class User {
@OneToMany(mappedBy = "user")
@JsonManagedReference // Included in User JSON
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JsonBackReference // Excluded from Order JSON
private User user;
}
3. @JsonView
Dynamically includes or excludes entity fields based on declarative serialization profiles defined on controller methods.
🟡 Middle Level
Preserving References with @JsonIdentityInfo
When business requirements demand that both sides of an association are represented in the API response, @JsonIdentityInfo serializes the full entity payload on its first occurrence, and substitutes its primary key ID on all subsequent cyclic references:
@Entity
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class User {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orders;
}
@Entity
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
Serialized JSON Output:
{
"id": 1,
"name": "Alice",
"orders": [
{
"id": 10,
"user": 1
}
]
}
(Notice that user is represented as the scalar ID 1 instead of an embedded recursive object).
🔴 Senior Level
Architectural Standard: Why Entities Must Never Be Exposed in APIs
Decorating JPA entities with Jackson annotations is a recognized Code Smell and technical debt. In production-grade software architecture, persistence entities must never cross the service boundary into the presentation layer.
5 Destructive Consequences of Exposing Entities in Controllers:
LazyInitializationException: Jackson accesses uninitialized lazy properties (FetchType.LAZY) during HTTP response streaming, outside of an active@Transactionalboundary when the HibernateSessionis already closed.- The Open-Session-In-View (OSIV) Trap: Teams enable
spring.jpa.open-in-view=trueto suppress lazy loading exceptions. As a result, slow network clients hold database connections from the HikariCP pool hostage throughout the entire HTTP request lifecycle, leading to pool starvation and catastrophic service outages under load. - Serialization $N+1$ Query Storms: Jackson’s reflection-based traversal triggers lazy collection loading for every item, executing hundreds of individual SQL queries per HTTP request.
- Security & Data Exposure (Overfetching): Sensitive fields (password hashes, internal security roles, soft-deletion flags) are accidentally leaked to public clients.
- Brittle API Contracts: Renaming or restructuring a database column instantly breaks mobile and web client contracts.
The Canonical Pattern: DTO Projections & Java Records
Design specialized, immutable Data Transfer Objects (Java Records) tailored to each API use case:
// Immutable DTO without circular references or ORM coupling:
public record UserResponseDto(
Long id,
String name,
List<OrderSummaryDto> orders
) {
public record OrderSummaryDto(Long id, BigDecimal amount, String status) {}
}
Project directly from the database using JPQL constructor expressions:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("""
SELECT new com.example.dto.UserResponseDto$OrderSummaryDto(o.id, o.amount, o.status)
FROM Order o
WHERE o.user.id = :userId
""")
List<UserResponseDto.OrderSummaryDto> findOrdersByUserId(@Param("userId") Long userId);
}
Production Advantages:
- The database fetches only the required columns (
SELECT id, amount, status). - Zero snapshot management overhead in the L1 persistence context.
- Zero circular references, zero
LazyInitializationExceptions, and strict contract stability.
4 Tricky Questions
1. Why does Jackson throw InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor when returning an entity with a lazy association?
Answer:
- Under
FetchType.LAZY, an uninitialized association does not contain the business domain object, but rather a dynamic proxy generated by ByteBuddy. - This proxy class contains internal Hibernate infrastructure properties (including the
ByteBuddyInterceptorhandler), which lack standard public JavaBean getter methods. - When Jackson attempts reflection-based serialization on the proxy object, it finds private fields without getters and throws
InvalidDefinitionException. - Workaround: Register the
jackson-datatype-hibernate6module or annotate the entity with@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}). - Senior Standard: Map the entity to a DTO in the service layer instead of returning ORM proxies.
2. What is the fundamental difference between @JsonIgnore and @JsonBackReference during DESERIALIZATION (@RequestBody)?
Answer:
@JsonIgnore: Completely suppresses the field in both directions. When deserializing incoming JSON, the ignored field remainsnull, leaving the object reference unpopulated.@JsonBackReference: Is bidirectional-aware during deserialization. When Jackson deserializes a parent object annotated with@JsonManagedReferenceand child objects annotated with@JsonBackReference, it automatically injects the parent reference into the child’s field, restoring the bidirectional link in memory during JSON parsing.
3. Why is @JsonIdentityInfo considered an anti-pattern for public REST APIs consumed by frontend or mobile clients?
Answer:
- API clients (written in TypeScript, Kotlin, Swift) depend on strict, deterministic schema contracts (OpenAPI/Swagger).
- With
@JsonIdentityInfo, the first appearance of an entity is rendered as a full JSON object ({ "id": 1, "name": "Alice" }), while all subsequent occurrences of the same entity in the same response payload are rendered as scalar integers (1). - This creates a polymorphic union type (
User | number), forcing frontend developers to implement custom client-side normalization logic and complicating automated API client generation.
4. Does placing Spring’s @Transactional annotation on a @RestController method prevent LazyInitializationException during Jackson serialization?
Answer:
- No, it does not.
- Spring MVC invokes the controller method within the transaction boundary. When the controller method returns the entity object, the transaction commits and closes the Hibernate session.
- JSON serialization via Jackson (
HttpMessageConverter) occurs AFTER the controller method has returned, during HTTP response body writing. - Because the persistence context is already closed when Jackson invokes getters on lazy associations, a
LazyInitializationExceptionis thrown (unless the dangerousopen-in-viewpattern is enabled).
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“Infinite recursion occurs when Jackson uses reflection to serialize bidirectional JPA entities, endlessly traversing reciprocal getters between parent and child until a
StackOverflowErroris triggered. Jackson provides annotations like@JsonIgnore,@JsonManagedReference/@JsonBackReference, and@JsonIdentityInfoto break the cycle. However, the senior architectural standard is to never expose JPA entities in REST controllers. Exposing entities causesLazyInitializationException, $N+1$ query storms during JSON rendering, sensitive data leakage, and forces dangerous workarounds like Open-Session-In-View. Use immutable DTOs (Java Records) mapped via MapStruct or JPQL constructor expressions instead.”
Key Production Metrics & Indicators
- StackOverflowError Incidents: Immediate indicators of circular reflection loops in serialization or logging frameworks.
- HTTP Response Rendering Latency: High delays between controller return and response write indicate Jackson triggering lazy-loading database queries during serialization.
- HikariCP Connection Hold Time: Disproportionately long connection lease durations pointing to
open-in-view=true.
Red Flags (What NOT to Say)
- ❌ “I switch all collections to
FetchType.EAGERso Jackson doesn’t crash.” — Disastrous; causes massive query storms and out-of-memory errors. - ❌ “Returning entities directly from REST controllers is fine as long as you add
@JsonIgnoreto sensitive fields.” — Brittle, couples database schema to external API contracts, and invites security leaks. - ❌ “I enable
spring.jpa.open-in-view=trueto fix serialization issues.” — Exhausts the database connection pool under production concurrency. - ❌ ”
@JsonManagedReferencebelongs on the child@ManyToOneside.” — Inverted;@JsonManagedReferencebelongs on the parent,@JsonBackReferenceon the child.
Related Topics
- What Are the Peculiarities of Bidirectional Relationships — Owning vs inverse sides
- What is Projection in JPA — Interface and Record projections
- What is LazyInitializationException and How to Avoid It — Session lifecycle management
- How to Properly Use @OneToMany and @ManyToOne — Relationship design