I have a many to one relation between two models in a .NET Web API Entity Framework project. Each model has a Navigation property to the other model. This as expected generates a "JsonException: A possible object cycle was detected." error.
When I remove the Navigation property from Vendor, I get the expected JSON from the Product controller.
var products = await _context.Products.Include(p => p.Vendor).ToListAsync();
The JSON from the Product API is as expected. Without the Navigation property in the Vendor model, I only get the Vendor properties.
Product controller JSON returned without the Navigation property in the Vendor model. The is the JSON I am expecting.
[
{
"id": 1,
"vendorID": 1,
"partNumber": "PN001",
"name": "Big Green Widget",
"price": 12.95,
"unit": "Each",
"photoPath": null,
"vendor": {
"id": 1,
"code": "T100",
"name": "Test Vendor",
"address": "123 somewhere st.",
"city": "Seattle",
"state": "WA",
"zip": "98765",
"phone": "123-123-1233",
"email": "[email protected]"
}
},
...
I added the following to Program.cs:
builder.Services.AddControllers().AddJsonOptions(opt =>
{
opt.JsonSerializerOptions.ReferenceHandler =
System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
});
With the Navigation properties in both models I now get the following JSON from the Product controller. When returning a Product it also returns the related Vendor, but also returns a Product collection for the Vendor. This collection is unexpected and is also odd in that it returns a Null product and a random Product from the Vendor’s related Products.
[
{
"id": 1,
"vendorID": 1,
"partNumber": "PN001",
"name": "Big Green Widget",
"price": 12.95,
"unit": "Each",
"photoPath": null,
"vendor": {
"id": 1,
"code": "T100",
"name": "Test Vendor",
"address": "123 somewhere st.",
"city": "Seattle",
"state": "WA",
"zip": "98765",
"phone": "123-123-1233",
"email": "[email protected]",
"products": [
null,
{
"id": 2,
"vendorID": 1,
"partNumber": "PN002",
"name": "Big Red Widget",
"price": 12.95,
"unit": "Each",
"photoPath": null,
"vendor": null
}
]
}
},
My questions:
- Is this a bug with System.Text.Json?
- If not, how can I get the correct JSON with both Navigation properties?
Versions:
- .NET 6 and also tested with 7 and 8.
- Matching version Entity Framework.
- The default System.Text.Json, but also tested newer versions from Nuget.
Models:
public class Vendor
{
[Key]
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
// navigation property
public List<Product>? Products { get; set; }
}
public class Product
{
[Key]
public int Id { get; set; }
public int VendorID { get; set; }
public string PartNumber { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Unit { get; set; }
public string? PhotoPath { get; set; }
// navigation property
public Vendor? Vendor { get; set; }
}
2
Answers
I think I now better understand this.
It's not a bug, but not how I understood the documentation. It does not limit cycles between DbSets, but limits cycles between objects. When it detects a cycle back to the same object it sets the property value to Null and stops drilling down.
So,
System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
is not too useful in my kinds of projects.If not, how can I get the correct JSON with both Navigation properties?
[JsonIgnore]
to the Vendor navigation property and use a LINQ Join.I’m not sure why it would be adding a #null, but setting the reference handler to "Ignore" would not prevent the Vendor.Products from being "touched" by the serializer in the case of lazy loading being enabled, or serializing any related entities that might be tracked otherwise. It would just prevent re-inserting instances of items that it has already inserted to avoid circular reference loops. The alternative is to use the "Preserve" option which would insert a $ref :{id} JSON reference, though this has limitations on whether the deserializer recognizes and reconstructs the reference.
The best option I can recommend to ensure that serialization results in the JSON you expect is to define DTOs for the desired object model and project the entities to the DTOs using
Select()
orProjectTo<TDTO>()
(Automapper, or similar with other mapping solutions) This way you can define the Product and Vendor DTOs with just the fields you want serialized, so the VendorDTO would not expose a Products collection, leaving you with clean, predictable JSON.