Introduction
Jackson throws a StackOverflowError when serializing objects with circular references -- for example, a Parent object that references its Child objects, which in turn reference back to the Parent. Jackson follows these references recursively until the stack overflows. This is a fundamental problem in object-relational mapping where bidirectional relationships (@ManyToOne and @OneToMany) are common. Without proper configuration, any attempt to serialize these entities to JSON causes the application to crash.
Symptoms
java.lang.StackOverflowError: null
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
at com.example.entity.Parent$HibernateProxy$...$JacksonSerializer.serialize(Unknown Source)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)
... (repeating Parent -> Child -> Parent pattern)Or with smaller objects:
java.lang.StackOverflowError
at com.fasterxml.jackson.databind.node.ObjectNode.put(ObjectNode.java:382)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:810)The error occurs when returning an entity from a REST endpoint:
@GetMapping("/parents/{id}")
public Parent getParent(@PathVariable Long id) {
return parentRepository.findById(id).orElseThrow();
// StackOverflowError when Jackson tries to serialize
}Common Causes
- Bidirectional JPA relationships:
@OneToManyon parent and@ManyToOneon child create a cycle - Parent-child tree structures: TreeNode objects referencing their parent node
- Graph data structures: Node objects with edges pointing to other nodes that point back
- Self-referencing entities: Employee references Manager who is also an Employee
- Hibernate proxies adding extra nesting: Lazy-loaded proxies add serialization complexity
- DTO with back-reference: DTOs that accidentally replicate the entity's circular structure
Step-by-Step Fix
Step 1: Use @JsonManagedReference and @JsonBackReference
```java @Entity public class Parent { @Id private Long id; private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) @JsonManagedReference // This side is serialized private List<Child> children = new ArrayList<>(); }
@Entity public class Child { @Id private Long id; private String name;
@ManyToOne @JoinColumn(name = "parent_id") @JsonBackReference // This side is NOT serialized (breaks the cycle) private Parent parent; } ```
The serialized output:
{
"id": 1,
"name": "Parent A",
"children": [
{"id": 10, "name": "Child 1"},
{"id": 11, "name": "Child 2"}
]
}Step 2: Use @JsonIdentityInfo for graph serialization
When you need both sides serialized but without infinite recursion:
```java @Entity @JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id" ) public class Department { @Id private Long id; private String name;
@OneToMany(mappedBy = "department") private List<Employee> employees = new ArrayList<>(); }
@Entity @JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class, property = "id" ) public class Employee { @Id private Long id; private String name;
@ManyToOne private Department department; } ```
Output:
{
"id": 1,
"name": "Engineering",
"employees": [
{"id": 10, "name": "Alice", "department": 1},
{"id": 11, "name": "Bob", "department": 1}
]
}Step 3: Use DTOs (recommended for REST APIs)
```java public record ParentDTO( Long id, String name, List<ChildDTO> children ) { public static ParentDTO fromEntity(Parent parent) { return new ParentDTO( parent.getId(), parent.getName(), parent.getChildren().stream() .map(ChildDTO::fromEntity) .toList() ); } }
public record ChildDTO(Long id, String name) { public static ChildDTO fromEntity(Child child) { return new ChildDTO(child.getId(), child.getName()); // Note: no parent reference in DTO } } ```
Prevention
- Never return JPA entities directly from REST controllers -- use DTOs
- If using entities directly, always add
@JsonManagedReference/@JsonBackReferenceto bidirectional relationships - Use
@JsonIgnoreon one side of the relationship as a quick fix - Add
@JsonIdentityInfofor graph-like data where references are needed - Configure
SerializationFeature.FAIL_ON_SELF_REFERENCESto catch cycles early - Add integration tests that serialize every entity returned by your REST endpoints