JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.73k stars 3.25k forks source link

ReferenceLoopHandling.Ignore unexpectedly skips objects - false positive if Equals/HashCode result is the same #2979

Open janseris opened 3 weeks ago

janseris commented 3 weeks ago

This is probably directly related to: https://github.com/JamesNK/Newtonsoft.Json/issues/401 The serializer detects false positive "reference loops" when there are more objects with the same Equals/Hashcode and not actually any reference loop. This is a bug. When ReferenceLoopHandling.Error is used, these false positives are blocking the code from executing. When ReferenceLoopHandling.Serialize is used, I receive access denied and crash. When ReferenceLoopHandling.Ignore is used, such objects are not serialized (skipped). I think the very dangerous error here might be that even if it's not the same BaseClass which causes my code to have false positive "reference loops" (actually matching Equals/Hashcode result), this can happen for random code where Equals/Hashcode collides because there are just 4 billion possible results and the collision can happen.

public class Building : NamedEntity
{
    public BuildingType Type { get; set; }
}

public class NamedEntity : INamedEntity 
{
}

public interface INamedEntity : IEntity, INamed
{
        public virtual int ID { get; set; }
        public virtual string Name { get; set; }

        public override bool Equals(object obj)
        {
            return obj is NamedEntity entity &&
                   ID == entity.ID;
        }

        public override int GetHashCode()
        {
            return 1213502048 + ID.GetHashCode();
        }
}

public interface IEntity
{
    int ID { get; set; }
}

public interface INamed
{
    string Name { get; set; }
}

public class BuildingType : NamedEntity
{
    public string Code { get; set; }
}

When Building ID 100 contains BuildingType ID 100 in original data, then the object BuildingType in Building ID 100 is not added to serialized JSON.

Similar situation:

public class GlobalRole : NamedEntity
{
        public List<GlobalUserRight> Rights { get; set; }
}

public class GlobalUserRight : NamedEntity 
{
}

When GlobalRole ID 1 contains GlobalRight ID 1, it is not serialized into the list of global rights (the one object with ID 1 is skipped).

Expected behavior

Objects are not skipped I suppose this is not a reference loop. There is no loop in 1:1 relation where 1 object contains another object and in 1:N where 1 object contains N objects. The inner object does not have any reference back to the outer object so there cannot be reference loop.

Actual behavior

Objects are skipped

Steps to reproduce

default ASP.NET Core Web API serialization with this setting: options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;