【问题标题】:What is equivalent in JToken.DeepEquals in System.Text.Json?System.Text.Json 中的 JToken.DeepEquals 中的等价物是什么?
【发布时间】:2020-03-07 18:11:25
【问题描述】:

我想将我的代码从 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 ) );
}

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

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 的格式无关紧要,属性的顺序也无关紧要。

【问题讨论】:

  • 你可以参考migration guide,如果没有什么有用的,你自己写吧,我觉得暂时不存在

标签: c# json.net system.text.json


【解决方案1】:

在 .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 值解析为 doubledecimal,因此 JToken.DeepEquals() 将仅在尾随零不同的浮点值视为相同。 IE。以下断言通过:

    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 完全支持重复的属性名称! IE。解析{"Value":"a", "Value" : "b"} 非常高兴,并将两个键/值对存储在文档中。

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

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

    此类利用池化内存中的资源来最大程度地减少垃圾收集器 (GC) 在高使用情况下的影响。未能正确处置此对象将导致内存未返回到池中,这将增加对框架各个部分的 GC 影响。

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

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

演示小提琴here.

【讨论】:

  • 优秀的答案,这就是我所需要的。非常感谢,使用中也
  • @dbc,你介意我把它放到我的Json.More 包中吗?我有一个实现; Equals 类似,但我喜欢 GetHashCode。我一定会在来源中保留指向此的链接以获得信用。
  • @gregsdennis - 当然,继续,我受宠若惊。请按照license进行属性。
  • 添加并发布,有一些修改。谢谢,干得好!
猜你喜欢
  • 2020-02-02
  • 1970-01-01
  • 2020-02-16
  • 2020-07-10
  • 2012-07-20
  • 2023-03-30
  • 2011-04-08
  • 2014-09-24
  • 2020-12-08
相关资源
最近更新 更多