1

I created a class that contained another class. Newtonsoft tells me there is a self referencing loop when JsonConvert.SerializeObject(translatedResponse) is called on the object. After trying various things, I changed the namespace name (which was 'DocumentGenerationParticipant', and created a DocumentGenerationParticipantResult for the plural class's List, instead of using the singular version that is also a DTO for a /participant endpoint. That worked, but I don't see why JSON serialization wouldn't work on this, and I see no circular reference? Did I miss something?

Program.CS that uses below DTOs:

using ConsoleApp4.DocumentGeneration;
using ConsoleApp4.DocumentGeneration.DocumentGenerationParticipant;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;

namespace ConsoleApp4
{
    static class Program
    {
        static void Main(string[] args)
        {
            var list = new List<DocumentGenerationParticipantResponse>()
            {
                new DocumentGenerationParticipantResponse("testName","testEmail","testUri","testRole"),
                new DocumentGenerationParticipantResponse("testName","testEmail","testUri","testRole"),
                new DocumentGenerationParticipantResponse("testName","testEmail","testUri","testRole")
            };
            var response = new DocumentGenerationParticipantsResponse(list);
            var result = JsonConvert.SerializeObject(response);
            Console.WriteLine(result);
        }
    }
}

DocumentGenerationParticipantResponse:

using System.Collections.Generic;

namespace ConsoleApp4.DocumentGeneration
{
    public class DocumentGenerationParticipantResponse
    {
        public string Name { get; }

        public string Email { get; }

        public string AccessUri { get; }

        public string Role { get; }

        public DocumentGenerationParticipantResponse(string name, string email, string accessUri, string role)
        {
            this.Name = name;
            this.Email = email;
            this.AccessUri = accessUri;
            this.Role = role;
        }

        public override bool Equals(object obj)
        {
            if (obj is null)
            {
                return false;
            }

            if (ReferenceEquals(this, obj))
            {
                return true;
            }

            DocumentGenerationParticipantResponse tmp = (DocumentGenerationParticipantResponse)obj;
            return Equals(this.Name, tmp.Name) &&
                   Equals(this.Email, tmp.Email) &&
                   Equals(this.AccessUri, tmp.AccessUri) &&
                   Equals(this.Role, tmp.Role);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = -2099;
                hashCode = hashCode * -2399 + EqualityComparer<string>.Default.GetHashCode(Name);
                hashCode = hashCode * -2399 + EqualityComparer<string>.Default.GetHashCode(Email);
                hashCode = hashCode * -2399 + EqualityComparer<string>.Default.GetHashCode(AccessUri);
                hashCode = hashCode * -2399 + EqualityComparer<string>.Default.GetHashCode(Role);
                return hashCode;
            }
        }
    }
}

DocumentGenerationParticipantsResponse:

using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp4.DocumentGeneration.DocumentGenerationParticipant
{
    public class DocumentGenerationParticipantsResponse
    {
        public List<DocumentGenerationParticipantResponse> Participants { get; }

        public DocumentGenerationParticipantsResponse(List<DocumentGenerationParticipantResponse> participants)
        {
            Participants = participants;
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            if (!(obj is List<DocumentGenerationParticipantResponse> list))
            {
                return false;
            }

            if (list.Count != this.Participants.Count)
            {
                return false;
            }

            return this.Participants.SequenceEqual(list);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = -2099;
                this.Participants.ForEach(x => hashCode = hashCode * x.GetHashCode());
                return hashCode;
            }
        }
    }
}

Error Message: "Message": "An error has occurred.", "ExceptionMessage": "Self referencing loop detected for property 'Participants' with type 'System.Collections.Generic.List`1[XXXXXX.Api.Management.Contracts.DocumentGeneration.DocumentGenerationParticipantResponse]'. Path ''.",

Sean Sherman
  • 327
  • 1
  • 16
  • Can you [edit] your question to share a [mcve]? – dbc Sep 18 '18 at 19:14
  • repeated - https://stackoverflow.com/questions/7397207/json-net-error-self-referencing-loop-detected-for-type – Nirav Parmar Sep 18 '18 at 19:16
  • I'll add something to make it verifiable. It's not a repeat of the listed question as that question doesn't ask *where* the circular reference is it seems to suggest fixing ones that exist. – Sean Sherman Sep 18 '18 at 19:19
  • You override `Equals()` which is what the reference loop detection uses to check for circular references. See [Why doesn't reference loop detection use reference equality?](https://stackoverflow.com/q/46936395). – dbc Sep 18 '18 at 19:46

1 Answers1

1

Your problem is that you overrode DocumentGenerationParticipantsResponse.Equals(object obj) to make an object of type DocumentGenerationParticipantsResponse equal to one of its own properties:

public class DocumentGenerationParticipantsResponse
{
    public List<DocumentGenerationParticipantResponse> Participants { get; }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }

        if (!(obj is List<DocumentGenerationParticipantResponse> list))
        {
            return false;
        }

        if (list.Count != this.Participants.Count)
        {
            return false;
        }

        return this.Participants.SequenceEqual(list);
    }

    // Remainder snipped

Specifically it will be equal to any List<DocumentGenerationParticipantResponse> with the same contents as its own Participants list. This explains the exception you are seeing. As explained in this answer to Why doesn't reference loop detection use reference equality?, Json.NET uses object equality rather than reference equality in checking for circular references. Thus, when serializing the Participants property, Json.NET thinks it is equal to the parent DocumentGenerationParticipantsResponse, and throws the exception you are seeing.

If you don't want to use object equality when checking for circular references, you can override JsonSerializerSettings.EqualityComparer and replace the default behavior of calling object.Equals in reference loop detection with something else, e.g. ReferenceEqualityComparer from this answer to IEqualityComparer<T> that uses ReferenceEquals by AnorZaken. But before doing that, you should examine your Equals() methods to make sure they are really doing what you want, since in this case DocumentGenerationParticipantsResponse.Equals() looks broken as the equality method is returning true for objects of different type.

(And, as noted in this answer by Daniel Gimenez, DocumentGenerationParticipantResponse.Equals() also looks broken. Among other issues it does not check for the incoming object obj being of the correct type.)

Further reading:

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks. I fixed up up the Equals and it runs fine. That seems to be root cause of the issue, the broken Equals override. With those fixed up, I can write out the serialized output. Accepted answer. – Sean Sherman Sep 18 '18 at 20:09