在System.Text.Json中,与JToken.DeepEquals等效的是什么?

18

我希望将我的代码从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的格式不重要,属性的顺序也不重要。


如果没有帮助,您可以参考迁移指南,否则您需要自己编写代码,我认为目前还没有现成的解决方案。 - Pavel Anikhouski
2个回答

18
在.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://learn.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值解析为doubledecimal, 因此JToken.DeepEquals()认为只有尾随零不同的浮点值是相同的。即以下断言将会通过:

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

我的比较器不认为这两个值相等。我认为这是合理的,因为应用程序有时希望保留尾随零,例如在反序列化为decimal时,因此这种差异有时很重要。(有关示例,请参见例如*Json.Net not serializing decimals the same way twice)如果您想将这样的JSON值视为相同,则需要修改ComputeHashCode()Equals(JsonElement x,JsonElement y)JsonValueKind.Number的情况,在小数点后出现尾随零时要修剪它们。

更让人惊讶的是,JsonDocument竟然完全支持重复的属性名!也就是说,它非常乐意解析{"Value":"a", "Value" : "b"}并将两个键/值对存储在文档内。

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

需要注意的是JsonDocument是可以释放的,根据文档,它确实需要被释放:

在高使用情况下,此类利用来自池化内存的资源以最小化垃圾收集器(GC)的影响。未正确释放此对象将导致内存未返回到池中,这将增加框架不同部分的GC影响。

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

当前有一个开放式的改进 System.Text.Json: add ability to do semantic comparisons of JSON values à la JToken.DeepEquals() #33388,但开发团队回答说,“目前我们的路线图上没有这个。”

演示fiddle在这里


非常好的答案,这正是我所需要的。非常感谢,使用起来也很棒。 - György Gulyás
@dbc,您介意我将此内容合并到我的Json.More包中吗?我已经有了一个实现;Equals方法类似,但我喜欢GetHashCode方法。我会在源代码中保留此链接以表明出处。 - gregsdennis
1
@gregsdennis - 当然可以,我很荣幸。请按照许可证进行归属。 - dbc
1
已添加和发布,进行了一些修改。谢谢,干得好! - gregsdennis

2

更新

自从我发布了 SystemTextJson.JsonDiffPatch NuGet 包的 1.3.0 版本以来,您可以使用 DeepEquals 扩展方法 来比较 JsonDocumentJsonElementJsonNode

以下是原始答案

对于自 .NET 6 发布以来引入的 System.Text.Json.Nodes 命名空间,目前有一个 Github 问题 讨论是否将 DeepEquals 功能添加到 JsonNode 中。

我在我的SystemTextJson.JsonDiffPatch NuGet软件包中自己实现了DeepEquals。默认情况下,该扩展将比较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);

谢谢!我需要DeepEquals()和DeepClone()两个函数。 - Tim Cooper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接