Careeroza
toString and JSON Loops
Infinite Recursion
With a bidirectional relationship, a generated toString() (Lombok @ToString, IDE-generated) or Jackson serialisation goes Order → items → OrderItem → order → items → ... until the stack overflows. On a managed entity it may also fire lazy-load queries along the way.
Example
Two plain classes that point to each other, each printing the other in toString():
import java.util.*;
public class Main {
public static void main(String[] args) {
Order order = new Order(5001L);
order.items.add(new OrderItem(1L, "Atta 5kg", order));
System.out.println("printing the order...");
System.out.println(order);
}
}
class Order {
Long id;
List<OrderItem> items = new ArrayList<>();
Order(Long id) { this.id = id; }
@Override public String toString() { return "Order{id=" + id + ", items=" + items + "}"; }
}
class OrderItem {
Long id; String product; Order order;
OrderItem(Long id, String product, Order order) { this.id = id; this.product = product; this.order = order; }
@Override public String toString() { return "OrderItem{id=" + id + ", product=" + product + ", order=" + order + "}"; }
}Output
printing the order... Exception: java.lang.StackOverflowError
Fixes
- Write
toString()by hand with only basic fields (id, name, status), never associations. With Lombok use@ToString(onlyExplicitlyIncluded = true)and@ToString.Includeon safe fields. - Never return entities from controllers. Return DTOs / records: no loops, no lazy loading during serialisation, and no accidental exposure of fields like
passwordHash. - If you must serialise entities, break the cycle with
@JsonManagedReference/@JsonBackReferenceor@JsonIgnoreon the back side.
@Override
public String toString() {
return "OrderItem{id=" + id + ", quantity=" + quantity + "}"; // no order, no product
}
public record OrderItemDto(Long id, String productName, int quantity, BigDecimal price) {}