Skip to main content

Fixed: "A possible object cycle was detected" When Returning EF Core Entities from a Web API

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

An endpoint that returned EF Core entities answered 500, with JsonException: A possible object cycle was detected. The path in the message gave it away: $.Products.Category.Products.Category.Products…. A category has products, and each product points back to its category, so the JSON would never end. The fix isn't a JSON setting: it's not returning entities in the first place.

What I Was Doing​

In a .NET 10 Web API with EF Core 10, I returned categories with their products from an endpoint, loading the products with Include:

src/Dewiride.Inventory.Api/Program.cs (temporary endpoint)
app.MapGet("/demo/entity-cycle", async (InventoryDbContext db) =>
{
var categories = await db.Categories.Include(c => c.Products).ToListAsync();
return Results.Ok(categories);
});

The two entities point at each other, as navigations normally do:

src/Dewiride.Inventory.Data/Entities (excerpts)
// Category
public ICollection<Product> Products { get; } = [];

// Product
public Category Category { get; set; } = null!;

What I Expected​

200 OK, with each category and a list of its products.

What I Got​

Response
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"traceId": "00-01fed7d34d160ef7fe429033a93d1f38-1b77050af5b1a51f-00"
}
API log (the path is shortened)
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 64. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.Products.Category.Products.Category.Products.Category.Products.Category…

A terminal with a 500 response, and the API log with System.Text.Json.JsonException: A possible object cycle was detected, and a path that repeats Products.Category over and over.

What I Checked​

Whether the query worked. It did. The log shows the LEFT JOIN ran before the failure, so the data was fine, and this is about writing the JSON:

API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Id`, `c`.`CreatedAtUtc`, `c`.`Name`, `p`.`Id`, `p`.`ArchivedAtUtc`, `p`.`CategoryId`, `p`.`CreatedAtUtc`, `p`.`Description`, `p`.`IsArchived`, `p`.`Name`, `p`.`QuantityOnHand`, `p`.`ReorderLevel`, `p`.`Sku`, `p`.`UnitPrice`, `p`.`UpdatedAtUtc`, `p`.`Version`
FROM `categories` AS `c`
LEFT JOIN `products` AS `p` ON `c`.`Id` = `p`.`CategoryId`
ORDER BY `c`.`Id`

Why the products point back. I only asked for c.Products, but EF Core also fills in Product.Category by itself. EF Core's eager loading page has a tip on this: "Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance." So each product's Category was set, and that category's Products contains the product.

Which option the message suggests. ReferenceHandler.Preserve adds $id and $ref properties to the JSON, which most clients don't expect. System.Text.Json's page also describes IgnoreCycles: "The serializer sets circular reference properties to null". I tried it, to see what the client would get:

src/Dewiride.Inventory.Api/Program.cs (excerpt)
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
Response body (the start)
[
{
"id": 1,
"name": "Cables",
"createdAtUtc": "2026-11-04T11:42:43.821656Z",
"products": [
{
"id": 1,
"sku": "CBL-USBC-2M",
"name": "USB-C cable, 2 m",
"description": "Braided, 100 W",
"categoryId": 1,
"category": null,

It answers 200. But every column of every product goes out, including internal ones, and "category": null is misleading: the product does have a category.

Why It Happens​

System.Text.Json writes an object by writing all its public properties, and follows them as deep as they go. Category → Products → Category → Products never ends, so it stops at its limit of 64 levels and reports a cycle. The entities are designed for EF Core, and those navigations are useful there. They just aren't a description of what your API should send.

The Fix​

Return a record that describes the response, and let Select build it. Here, each category with the number of products it has:

An endpoint that returns what the client needs
var categories = await db.Categories
.OrderBy(c => c.Name)
.Select(c => new { c.Name, ProductCount = c.Products.Count })
.ToListAsync();

return Results.Ok(categories);

There's no Include, because Select tells EF Core what's needed. In a real API, use a named record instead of an anonymous type, as in How to Select Only the Columns You Need.

After the Fix​

Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
{
"name": "Adapters",
"productCount": 2
},
{
"name": "Cables",
"productCount": 4
},
{
"name": "Chargers",
"productCount": 2
}
]
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Name`, (
SELECT COUNT(*)
FROM `products` AS `p`
WHERE `c`.`Id` = `p`.`CategoryId`) AS `ProductCount`
FROM `categories` AS `c`
ORDER BY `c`.`Name`

A terminal with a 200 OK response listing each category's name and product count, and the single SELECT with a COUNT subquery that EF Core sent.

Smaller JSON, a cheaper query, and no way for a new navigation to change your API by accident.

Common Mistakes​

MistakeWhat happensDo this instead
Using IgnoreCycles and carrying onEntities still leak every column, and some navigations come out nullReturn records
Using [JsonIgnore] on navigationsIt works, until an endpoint needs that navigationKeep JSON concerns out of entities
Using PreserveClients have to understand $id and $refReturn records
Removing the navigation from the entityQueries and configuration lose itKeep it; don't serialize the entity

Troubleshooting​

SymptomLikely causeFix
A possible object cycle was detected with a repeating pathEntities with two-way navigations in the responseReturn records
The same message with a long path that doesn't repeatReal data nested deeper than 64 levelsRaise MaxDepth, or flatten the response
No error, but "category": null on productsThe navigation wasn't loaded, or IgnoreCycles cut itProject the value you need
The error appears only on some rowsOnly rows with related data loaded have a cycleThe same fix

FAQ​

Why did it work without Include?​

Without Include, Products stays empty, and there's no cycle to follow. The error appears as soon as both ends are loaded.

Should a record contain another record?​

Yes, if the client needs it, such as a category with a list of product summaries. Records only contain what you put in them, so there's no way back up.

Conclusion​

"A possible object cycle was detected" means the JSON writer followed your entities' navigations in a circle, usually after Include. JSON options can hide it, but they keep sending every column. Use Select with a response record, and the cycle can't exist.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.