JToken.DeepEquals 在 System.Text.Json 中的等价物是什么?

What is equivalent in JToken.DeepEquals in System.Text.Json?

我想将我的代码从 Newtonsoft Json.Net 迁移到 Microsoft 标准 System.Text.Json。但是我找不到 JToken.DeepEqual

的替代方案

基本上代码必须在单元测试中比较两个 JSON。引用 JSON,结果 JSON。我使用 Newtonsoft 中的机制创建两个 JObject,然后将它们与 JToken.DeepEqual 进行比较。这是示例代码:

[TestMethod]
public void ExampleUnitTes()
{
    string resultJson = TestedUnit.TestedMethod();
    string referenceJson =
    @"
    {
      ...bla bla bla...
      ...some JSON Content...
      ...bla bla bla...
    }";

    JObject expected = ( JObject )JsonConvert.DeserializeObject( referenceJson );
    JObject result = ( JObject )JsonConvert.DeserializeObject( resultJson );
    Assert.IsTrue( JToken.DeepEquals( result, expected ) );
}

如果我是正确的 Newtonsoft JObject 类似于 System.Text.Json.JsonDocument,并且我能够创建它,只是我不知道如何比较它的内容。

System.Text.Json.JsonDocument expectedDoc = System.Text.Json.JsonDocument.Parse( referenceJson );
System.Text.Json.JsonDocument resultDoc = System.Text.Json.JsonDocument.Parse( json );

Compare???( expectedDoc, resulDoc );

当然,字符串比较不是解决方案,因为 JSON 的格式无关紧要,属性的顺序也无关紧要。

自 .Net 3.1 起,System.Text.Json 中没有等效项,因此我们必须自己动手。这是一种可能 IEqualityComparer<JsonElement>:

public class JsonElementComparer : IEqualityComparer<JsonElement>
{
    public JsonElementComparer() : this(-1) { }

    public JsonElementComparer(int maxHashDepth) => this.MaxHashDepth = maxHashDepth;

    int MaxHashDepth { get; } = -1;

    #region IEqualityComparer<JsonElement> Members

    public bool Equals(JsonElement x, JsonElement y)
    {
        if (x.ValueKind != y.ValueKind)
            return false;
        switch (x.ValueKind)
        {
            case JsonValueKind.Null:
            case JsonValueKind.True:
            case JsonValueKind.False:
            case JsonValueKind.Undefined:
                return true;
                
            // Compare the raw values of numbers, and the text of strings.
            // Note this means that 0.0 will differ from 0.00 -- which may be correct as deserializing either to `decimal` will result in subtly different results.
            // Newtonsoft's JValue.Compare(JTokenType valueType, object? objA, object? objB) has logic for detecting "equivalent" values, 
            // you may want to examine it to see if anything there is required here.
            // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Linq/JValue.cs#L246
            case JsonValueKind.Number:
                return x.GetRawText() == y.GetRawText();

            case JsonValueKind.String:
                return x.GetString() == y.GetString(); // Do not use GetRawText() here, it does not automatically resolve JSON escape sequences to their corresponding characters.
                
            case JsonValueKind.Array:
                return x.EnumerateArray().SequenceEqual(y.EnumerateArray(), this);
            
            case JsonValueKind.Object:
                {
                    // Surprisingly, JsonDocument fully supports duplicate property names.
                    // I.e. it's perfectly happy to parse {"Value":"a", "Value" : "b"} and will store both
                    // key/value pairs inside the document!
                    // A close reading of https://www.rfc-editor.org/rfc/rfc8259#section-4 seems to indicate that
                    // such objects are allowed but not recommended, and when they arise, interpretation of 
                    // identically-named properties is order-dependent.  
                    // So stably sorting by name then comparing values seems the way to go.
                    var xPropertiesUnsorted = x.EnumerateObject().ToList();
                    var yPropertiesUnsorted = y.EnumerateObject().ToList();
                    if (xPropertiesUnsorted.Count != yPropertiesUnsorted.Count)
                        return false;
                    var xProperties = xPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
                    var yProperties = yPropertiesUnsorted.OrderBy(p => p.Name, StringComparer.Ordinal);
                    foreach (var (px, py) in xProperties.Zip(yProperties))
                    {
                        if (px.Name != py.Name)
                            return false;
                        if (!Equals(px.Value, py.Value))
                            return false;
                    }
                    return true;
                }
                
            default:
                throw new JsonException(string.Format("Unknown JsonValueKind {0}", x.ValueKind));
        }
    }

    public int GetHashCode(JsonElement obj)
    {
        var hash = new HashCode(); // New in .Net core: https://docs.microsoft.com/en-us/dotnet/api/system.hashcode
        ComputeHashCode(obj, ref hash, 0);
        return hash.ToHashCode();
    }

    void ComputeHashCode(JsonElement obj, ref HashCode hash, int depth)
    {
        hash.Add(obj.ValueKind);

        switch (obj.ValueKind)
        {
            case JsonValueKind.Null:
            case JsonValueKind.True:
            case JsonValueKind.False:
            case JsonValueKind.Undefined:
                break;
                
            case JsonValueKind.Number:
                hash.Add(obj.GetRawText());
                break;

            case JsonValueKind.String:
                hash.Add(obj.GetString());
                break;
                
            case JsonValueKind.Array:
                if (depth != MaxHashDepth)
                    foreach (var item in obj.EnumerateArray())
                        ComputeHashCode(item, ref hash, depth+1);
                else
                    hash.Add(obj.GetArrayLength());
                break;
            
            case JsonValueKind.Object:
                foreach (var property in obj.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
                {
                    hash.Add(property.Name);
                    if (depth != MaxHashDepth)
                        ComputeHashCode(property.Value, ref hash, depth+1);
                }
                break;
                
            default:
                throw new JsonException(string.Format("Unknown JsonValueKind {0}", obj.ValueKind));
        }            
    }
    
    #endregion
}

使用方法如下:

var comparer = new JsonElementComparer();
using var doc1 = System.Text.Json.JsonDocument.Parse(referenceJson);
using var doc2 = System.Text.Json.JsonDocument.Parse(resultJson);
Assert.IsTrue(comparer.Equals(doc1.RootElement, doc2.RootElement));

备注:

  • 由于Json.NET解析浮点JSON值解析为doubledecimalJToken.DeepEquals()考虑浮点仅尾随零不同的值是相同的。 IE。以下断言通过:

    Assert.IsTrue(JToken.DeepEquals(JToken.Parse("1.0"), JToken.Parse("1.00")));
    

    我的比较器认为这两者相等。我认为这是可取的,因为应用程序有时希望保留尾随零,例如当反序列化为 decimal 时,因此这种差异有时可能很重要。 (有关示例,请参见 *)如果您想将此类 JSON 值视为相同,则需要修改 ComputeHashCode()JsonValueKind.Number 中的大小写 Equals(JsonElement x, JsonElement y) 到 trim 小数点后的尾随零。

  • 更难的是,令人惊讶的是,JsonDocument 完全支持重复的 属性 名称! IE。很高兴解析 {"Value":"a", "Value" : "b"} 并将两个 key/value 对存储在文档中。

    仔细阅读 https://www.rfc-editor.org/rfc/rfc8259#section-4 似乎表明允许但不推荐使用此类对象,并且当它们出现时,对同名属性的解释可能取决于顺序。我通过按 属性 名称对 属性 列表进行稳定排序,然后遍历列表并比较名称和值来处理此问题。如果您不关心重复的 属性 名称,您可以通过使用单个查找字典而不是两个排序列表来提高性能。

  • JsonDocument是一次性的,实际上需要按照docs:

    进行处理

    This class utilizes resources from pooled memory to minimize the impact of the garbage collector (GC) in high-usage scenarios. Failure to properly dispose this object will result in the memory not being returned to the pool, which will increase GC impact across various parts of the framework.

    在你的问题中你没有这样做,但你应该这样做。

  • 目前有一个开放的增强功能 System.Text.Json: add ability to do semantic comparisons of JSON values à la JToken.DeepEquals() #33388,开发团队回复说,“这现在不在我们的路线图上。 “

演示 fiddle here.

更新

自从我的 SystemTextJson.JsonDiffPatch NuGet package, you can use DeepEquals extension method 版本 1.3.0 开始比较 JsonDocumentJsonElementJsonNode

原回答如下

System.Text.Json.Nodes namespace introduced since .NET 6 release, there is currently a Github issue 讨论将 DeepEquals 功能添加到 JsonNode

我将自己的 DeepEquals 实现作为 SystemTextJson.JsonDiffPatch NuGet 包的一部分。默认情况下,扩展比较 JSON 值的原始文本,这不是 JToken.DeepEquals 所做的。需要启用语义平等:

var node1 = JsonNode.Parse("[1.0]");
var node2 = JsonNode.Parse("[1]");

// false
bool equal = node1.DeepEquals(node2);
// true
bool semanticEqual = node1.DeepEquals(node2, JsonElementComparison.Semantic);