skip to Main Content

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


  1. Chosen as BEST ANSWER

    I think I now better understand this.

    • Is this a bug with System.Text.Json?

    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.

    [
        {
            "id": 1,                // this is Product1
            "vendorID": 1,
            "partNumber": "PN001",
            "name": "Big Green Widget",
            "price": 12.95,
            "unit": "Each",
            "photoPath": null,
            "vendor": {            // this is vendor for Product1
                "id": 1,
                "code": "AMZ",
                "name": "AMZ Inc.",
                "address": "123 somewhere st.",
                "city": "Seattle",
                "state": "WA",
                "zip": "98765",
                "phone": "123-123-1233",
                "email": "[email protected]",
                "products": [
                    null,           // this is the start of the cycle
                                    // back to Product1, so set to null
    
                    {               // This is the first of other
                                    // products from Vendor1
                        "id": 2,
                        "vendorID": 1,
                        "partNumber": "PN002",
                        "name": "Big Red Widget",
                        "price": 12.95,
                        "unit": "Each",
                        "photoPath": null,
                        "vendor": null       // this is the start 
                                             // of the cycle
                                             // for Vendor1 back
                                             // to Product1, so 
                                             // set to null
                    }
                ]
            }
        },
    

    • If not, how can I get the correct JSON with both Navigation properties?

      • Add [JsonIgnore] to the Vendor navigation property and use a LINQ Join.
      • Return the Vendor data using a DTO or LINQ anonymous type.
      • or other code solutions...

  2. 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() or ProjectTo<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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search