Diagnosing and Fixing JSON Circular Reference Errors in Jackson, Gson, System.Text.Json, and Newtonsoft.Json
When serializing object graphs with bidirectional relationships you often hit StackOverflowError or JsonException. This guide shows how to spot the culprit, map the cause to the right library, apply the correct annotations or options, and verify the fix.
08 Mar 2026, 11:47 UTC

Problem: Infinite Recursion When Serializing Bidirectional Object Graphs
When a parent object holds a reference to a child and the child holds a back-reference to the parent, most JSON serializers try to walk the graph forever. Java libraries throw StackOverflowError or JsonMappingException with messages like "Infinite recursion"; .NET libraries raise JsonException or InvalidOperationException saying "Self-referencing loop detected". The symptom is a hard crash, an exception that bubbles up to your API layer, or a stack trace that includes your domain classes.
What to Look For
- Exception type and message (StackOverflowError, JsonException, etc.)
- Stack trace points to the library's serializer code and then to your model class
- Payload size or latency spikes when the exception disappears
Cause-Diagnostic Table
| Cause | Library | Mitigation Annotation / Option |
|---|---|---|
| Bidirectional @OneToMany/@ManyToOne without back-reference control | Jackson | @JsonManagedReference / @JsonBackReference or @JsonIdentityInfo |
| Parent-child navigation without ignore on one side | System.Text.Json | JsonIgnore or ReferenceHandler.Preserve |
| Circular refs without reference handling | Newtonsoft.Json | PreserveReferencesHandling or JsonIgnore |
| Gson lacks built-in cycle handling | Gson | transient fields, @Expose with custom TypeAdapter, or DTO projection |
Step-by-Step Diagnostic Checklist
- Identify the exception - note its type, message, and the first user-defined class in the stack trace.
- Inspect the model - look for two-way navigation properties (e.g., List<Child> children and Parent parent).
- Verify annotations / attributes - ensure the library-specific controls are present on both sides.
- Confirm library version - some features (e.g., ReferenceHandler.Preserve) require .NET 6+ or Jackson 2.12+.
- Run a minimal reproducible test - create a parent, add a child, set back-reference, serialize, and assert that no exception occurs.
Library-Specific Fixes
Jackson (Java)
Use @JsonManagedReference on the collection in the parent and @JsonBackReference on the back-reference in the child. If you need both sides in the JSON, switch to @JsonIdentityInfo with an ObjectIdGenerators.PropertyGenerator.
@Entity
public class Parent {
@OneToMany(mappedBy = "parent")
@JsonManagedReference
private List<Child> children;
}
@Entity
public class Child {
@ManyToOne
@JsonBackReference
private Parent parent;
}
System.Text.Json (.NET)
Either ignore one side or enable reference preservation. The latter embeds $id and $ref tokens.
public class Parent {
public List<Child> Children { get; set; }
}
public class Child {
public Parent Parent { get; set; }
}
var options = new JsonSerializerOptions {
ReferenceHandler = ReferenceHandler.Preserve
};
string json = JsonSerializer.Serialize(parent, options);
Or add [JsonIgnore] to one navigation property.
Newtonsoft.Json (.NET)
Set PreserveReferencesHandling.Objects globally or use [JsonIgnore] on one side.
var settings = new JsonSerializerSettings {
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
string json = JsonConvert.SerializeObject(parent, settings);
Gson (Java)
Gson does not support cycle handling out of the box. Options include marking a field transient, using @Expose with a custom TypeAdapter, or projecting the domain model to a DTO that breaks the cycle.
public class Child {
@Expose(serialize = false)
private Parent parent;
}
Concrete Minimal Reproducible Test
For each library, write a single unit test that constructs a Parent and a Child, links them, serializes, and asserts that the output string is not null and does not contain the word "StackOverflow".
// Example for Jackson
@Test
public void testJacksonCycle() {
Parent p = new Parent();
Child c = new Child();
p.setChildren(Collections.singletonList(c));
c.setParent(p);
String json = new ObjectMapper().writeValueAsString(p);
assertNotNull(json);
assertFalse(json.contains("StackOverflow"));
}
Verification Checklist
- Run the test repeatedly and measure average latency; confirm no StackOverflowError occurs.
- Check payload size - ReferenceHandler.Preserve and PreserveReferencesHandling add $id/$ref tokens; ensure consumers can handle them.
- Use a JSON schema validator to confirm the output still matches the expected contract.
Limitations and Trade-offs
- Reference preservation changes the JSON format; legacy consumers that expect a flat structure will break.
- Using @JsonBackReference hides the back-reference from the output; if you need it, switch to @JsonIdentityInfo but beware of larger payloads.
- Gson's lack of built-in cycle handling means you must either restructure your model or write a custom adapter, which adds maintenance overhead.
- Projecting to DTOs removes the cycle entirely but requires mapping code or a library like MapStruct/AutoMapper.
- Circular references in your domain model often indicate missing aggregate boundaries; consider refactoring the model rather than only patching serialization.
Escalation Criteria
- The cycle spans multiple aggregate roots or business contexts - treat it as a domain design issue.
- Reference preservation doubles the JSON payload and increases latency - request a redesign of the API contract.
- Multiple libraries in the same project require inconsistent cycle handling strategies - standardize on a single approach or isolate the libraries.
- After applying the fix, serialization remains a bottleneck - profile memory usage and consider stream-based serialization or pagination.
Practical Takeaway
Identify the exception, map it to the library, apply the proper annotation or option, and run a minimal unit test that verifies both correctness and performance. Always confirm that the resulting JSON schema still satisfies downstream consumers and that the payload size is acceptable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.