在.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;
case JsonValueKind.Number:
return x.GetRawText() == y.GetRawText();
case JsonValueKind.String:
return x.GetString() == y.GetString();
case JsonValueKind.Array:
return x.EnumerateArray().SequenceEqual(y.EnumerateArray(), this);
case JsonValueKind.Object:
{
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();
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));
注意:
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在这里。