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.Include on 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/@JsonBackReference or @JsonIgnore on 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) {}