如何使用C#比较两个Json对象

32

我有两个Json对象需要进行比较。我正在使用Newtonsoft库进行Json解析。

string InstanceExpected = jsonExpected;
string InstanceActual = jsonActual;
var InstanceObjExpected = JObject.Parse(InstanceExpected);
var InstanceObjActual = JObject.Parse(InstanceActual);

我正在使用Fluent Assertions进行比较。但问题是,当属性数/名称不匹配时,Fluent Assertion会失败。如果JSON值不同,它会通过测试。我需要在值不同时也能失败。

我使用Fluent Assertions进行比较,但是它只有在属性数量/名称不匹配时才会失败。如果Json值不同,它会通过测试。我需要在值不同的情况下也能够失败。

InstanceObjActual.Should().BeEquivalentTo(InstanceObjExpected);
例如,我有以下实际和预期的JSON进行比较。使用上述比较方式使它们通过了测试,但这是错误的。

例如,我有以下实际和预期的JSON进行比较。使用上述比较方法会使它们通过测试,但这是错误的。

{
  "Name": "20181004164456",
  "objectId": "4ea9b00b-d601-44af-a990-3034af18fdb1%>"  
}

{
  "Name": "AAAAAAAAAAAA",
  "objectId": "4ea9b00b-d601-44af-a990-3034af18fdb1%>"  
}

2
为什么不直接比较字符串,而非比较JSON对象呢? - Rui Jarimba
1
一个简单的字符串比较不就可以了吗?jsonExpected == jsonActual - Olivier Jacot-Descombes
13
JSON 实际上不是一个字符串,所以上述评论无关。{ "id": "5" } 应该和 { "id" : "5" } 相同。因此,您不能使用字符串比较器来比较 JSON。 - Jesse de Wit
3
除非 JSON 总是由相同的过程创建且项目已排序。 - Olivier Jacot-Descombes
2
@JessedeWit...我不主张使用字符串比较,因为它很麻烦。然而,如果通过一个可以保证属性排序的序列化器进行往返传输,它可能会起作用。 - spender
显示剩余5条评论
7个回答

43

我进一步挖掘了一下,找到了为什么OP的测试代码不能按预期运行的原因。通过安装并使用FluentAssertions.Json Nuget包,我成功地解决了这个问题。

一个重要的提示:

确保包含using FluentAssertions.Json,否则可能会出现错误结果。

测试代码如下:

using FluentAssertions;
using FluentAssertions.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

[TestFixture]
public class JsonTests
{
    [Test]
    public void JsonObject_ShouldBeEqualAsExpected()
    {
        JToken expected = JToken.Parse(@"{ ""Name"": ""20181004164456"", ""objectId"": ""4ea9b00b-d601-44af-a990-3034af18fdb1%>"" }");
        JToken actual = JToken.Parse(@"{ ""Name"": ""AAAAAAAAAAAA"", ""objectId"": ""4ea9b00b-d601-44af-a990-3034af18fdb1%>"" }");

        actual.Should().BeEquivalentTo(expected);
    }
}

运行测试:

单元测试结果


1
是的。只包含FluentAssertions.Json对我来说很好用。我想知道没有它会有多糟糕。谢谢! - Nandakumar1712

38

考虑使用Newtonsoft提供的JToken.DeepEquals()方法。无论您使用哪个测试框架,它都会类似于以下内容:

Console.WriteLine(JToken.DeepEquals(InstanceObjActual, InstanceObjExpected));
// false

2
你比我快大约10秒钟 :) 这是正确的答案。 - spender
很棒的东西,这比我的解决方案简单和清晰多了。 - Rui Jarimba
4
我在单元测试中使用JToken.DeepEquals的唯一问题是无法找出JSON字符串之间的差异。 将JSON反序列化为C#对象并进行比较会提供更好的错误消息(请参见我附加到回答中的截图)。 - Rui Jarimba
2
当然,将其反序列化为C#对象可以提供更好的错误消息,但由于这是一个测试项目,我们处理各种Json响应,为每个响应创建类对象的工作量相当大且过于繁琐,因此我并没有在所有地方采用这种方法。 - Nandakumar1712
@Nandakumar1712 好的。检查我的答案,其中一个正好有你需要的内容,并且在测试失败时会给出合理的错误信息。 - Rui Jarimba
我尝试了 JToken.DeepEquals,但它返回了错误的结果。 - Mali Tbt

4

一种选择是将json字符串反序列化为C#对象并进行比较。

与使用JToken.DeepEquals(由@JessedeWit建议)相比,这种方法需要更多的工作,但具有如果测试失败则提供更好的错误消息的优点(请参见下面的屏幕截图)。

您的json字符串可以建模为以下类:

public class Entity
{
    [JsonProperty("Name")]
    public string Name { get; set; }

    [JsonProperty("objectId")]
    public string ObjectId { get; set; }
}

在你的测试中,将JSON字符串反序列化为对象并进行比较:
[TestFixture]
public class JsonTests
{
    [Test]
    public void JsonString_ShouldBeEqualAsExpected()
    {
        string jsonExpected = @"{ ""Name"": ""20181004164456"", ""objectId"": ""4ea9b00b-d601-44af-a990-3034af18fdb1%>"" }";
        string jsonActual = @"{ ""Name"": ""AAAAAAAAAAAA"", ""objectId"": ""4ea9b00b-d601-44af-a990-3034af18fdb1%>"" }";

        Entity expectedObject = JsonConvert.DeserializeObject<Entity>(jsonExpected);
        Entity actualObject = JsonConvert.DeserializeObject<Entity>(jsonActual);

        actualObject.Should().BeEquivalentTo(expectedObject);
    }
}

PS:我在测试方法中使用了NUnit和FluentAssertions。运行测试:

Unit test results


就像你所说的,这需要更多的工作,或者更糟糕的是,可能会在共享库周围产生很多耦合。 - user1496062
1
@user1496062 你给我的回答点了踩吗?仅仅因为这个方法需要更多的工作并不意味着它是一个糟糕的回答,相反地,正如我已经提到的,这种方法的优点在于你将会得到更有意义的错误信息。 - Rui Jarimba
这是关于将JSON转换为对象而增加耦合性而不是额外工作的问题。大多数我们使用的集成/端到端测试都没有这些对象,因此答案并不有用,可能会导致其他人出现更多的耦合/问题。 - user1496062
2
@user1496062,虽然这对你没有帮助,但对其他人可能有用 - 这就是我想说的。 - Rui Jarimba

4

我制作了一个非递归的方法来删除双胞胎——思路是从非常相似的JSON中删除相同的元素,以便每个对象中只留下不同的节点:

public void RemoveTwins(ref BreadthFirst bf1, ref BreadthFirst bf2) {
    JsonNode traversal = bf1.Next();
    Boolean removed = false;
    do {
        if (!removed) {
            if (bf2.Current != null) while (bf1.Level == bf2.Level && bf2.Next() != null) ;
            if (bf2.Current != null) while (bf1.Level != bf2.Level && bf2.Next() != null) ;
            else bf2.Current = bf2.root;
        }
        else traversal = bf1.Next();
        if (bf2.Level < 0) bf2.Current = bf2.Root;
        do {
            removed = bf1.NextAs(bf1.src, bf2, bf2.src);
            if (removed && bf1.Orphan && bf2.Orphan) {
                JsonNode same = bf1.Current.Parent;
                traversal = bf1.RemoveCurrent();
                same = bf2.Current.Parent;
                bf2.RemoveCurrent();
                bf1.UpdateLevel();
                bf2.UpdateLevel();
                if (traversal == null
                || bf1.Root == null || bf2.Root == null
                || (bf1.Level == 0 && bf1.Current.NodeBelow == null)) {
                    traversal = null;
                    break;
                }
            } else
            if (!removed) {
                break; 
            } else removed = false;
        } while (removed);
        if (!removed) traversal = bf1.Next();
    } while (traversal != null);
}

完整的代码和解析器在我的GitHub上(个人资料或下面链接)。
较旧的CSV版本也可以在我的这个问题中排序输入如何比较大JSON?(新版本不支持,因此当其中一个对象具有相反的顺序时,它可能非常缓慢-在解析期间排序或至少将两个孪生兄弟的邻居进行比较作为第一步搜索会更容易)


还有一个sort方法 => 不同顺序的JSON也可以被处理。 - Tom

0
你可以尝试这个。
    using Newtonsoft.Json.Linq;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    namespace test
    {
        public static class ExtensionMethod
        {
            public static JObject FindDiff(this JToken actual, JToken expected, ref List<string> changedItems)
            {
                var diff = new JObject();
                string valueToPrint = string.Empty;
                if (JToken.DeepEquals(actual, expected)) return diff;
                else if (actual.Type != expected.Type)
                {
                    return diff;
                }

                switch (actual.Type)
                {
                    case JTokenType.Object:
                        {
                            var Initial = actual as JObject;
                            var Updated = expected as JObject;
                            var addedKeys = Initial.Properties().Select(c => c.Name).Except(Updated.Properties().Select(c => c.Name));
                            var removedKeys = Updated.Properties().Select(c => c.Name).Except(Initial.Properties().Select(c => c.Name));
                            var unchangedKeys = Initial.Properties().Where(c => JToken.DeepEquals(c.Value, expected[c.Name])).Select(c => c.Name);
                            foreach (var k in addedKeys)
                            {
                                diff[k] = new JObject
                                {
                                    ["+"] = actual[k]
                                };
                            }
                            foreach (var k in removedKeys)
                            {
                                diff[k] = new JObject
                                {
                                    ["-"] = expected[k]
                                };
                            }
                            var potentiallyModifiedKeys = Initial.Properties().Select(c => c.Name).Except(addedKeys).Except(unchangedKeys);
                            foreach (var k in potentiallyModifiedKeys)
                            {
                                var foundDiff = FindDiff(Initial[k], Updated[k], ref changedItems);
                                if (foundDiff == null)
                                    return foundDiff;

                                if (foundDiff.HasValues && (foundDiff["+"] != null || foundDiff["-"] != null))
                                {
                                    //Execute when json element is an String
                                    if (IsValueChanged(foundDiff))
                                    {
                                        changedItems.Add($"actual value '{foundDiff["+"].ToString()}' is not equal to expected value '{foundDiff["-"].ToString()}'");
                                    }
                                    //Execute when json element is an Array
                                    else
                                    {
                                        for (int i = 0; i < foundDiff["+"].Count(); i++)
                                        {
                                            var foundDiffOfArray = FindDiff(foundDiff["+"][i], foundDiff["-"][i], ref changedItems);
                                            if (IsValueChanged(foundDiffOfArray))
                                            {
                                                changedItems.Add($"actual value '{foundDiff["+"].ToString()}' is not equal to expected value '{foundDiff["-"].ToString()}'");
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        break;
                    //"+" indicate the Original Value
                    //"-" indicate the Updated/Modified Value
                    case JTokenType.Array:
                        {
                            var current = actual as JArray;
                            var model = expected as JArray;
                            var plus = new JArray(current.ExceptAll(model, new JTokenEqualityComparer()));
                            var minus = new JArray(model.ExceptAll(current, new JTokenEqualityComparer()));
                            if (plus.HasValues) diff["+"] = plus;
                            if (minus.HasValues) diff["-"] = minus;
                        }
                        break;
                    default:
                        diff["+"] = actual;
                        diff["-"] = expected;
                        break;
                }

                return diff;
            }

            public static bool IsValueChanged(JObject foundDiff)
            {
                return (foundDiff["-"] != null && foundDiff["-"].Type == JTokenType.String)
                    || (foundDiff["+"] != null && foundDiff["+"].Type == JTokenType.String);
            }

            public static IEnumerable<TSource> ExceptAll<TSource>(
            this IEnumerable<TSource> first,
            IEnumerable<TSource> second)
            {
                return ExceptAll(first, second, null);
            }

            public static IEnumerable<TSource> ExceptAll<TSource>(
                this IEnumerable<TSource> first,
                IEnumerable<TSource> second,
                IEqualityComparer<TSource> comparer)
            {
                if (first == null) { throw new ArgumentNullException("first"); }
                if (second == null) { throw new ArgumentNullException("second"); }


                var secondCounts = new Dictionary<TSource, int>(comparer ?? EqualityComparer<TSource>.Default);
                int count;
                int nullCount = 0;

                // Count the values from second
                foreach (var item in second)
                {
                    if (item == null)
                    {
                        nullCount++;
                    }
                    else
                    {
                        if (secondCounts.TryGetValue(item, out count))
                        {
                            secondCounts[item] = count + 1;
                        }
                        else
                        {
                            secondCounts.Add(item, 1);
                        }
                    }
                }

                // Yield the values from first
                foreach (var item in first)
                {
                    if (item == null)
                    {
                        nullCount--;
                        if (nullCount < 0)
                        {
                            yield return item;
                        }
                    }
                    else
                    {
                        if (secondCounts.TryGetValue(item, out count))
                        {
                            if (count == 0)
                            {
                                secondCounts.Remove(item);
                                yield return item;
                            }
                            else
                            {
                                secondCounts[item] = count - 1;
                            }
                        }
                        else
                        {
                            yield return item;
                        }
                    }
                }
            }
        }
    }

-1

在将JSON反序列化为C#对象后,正确的方法是在反序列化类中实现IComparable接口并比较这两个对象。

因此:

using System;
using System.Collections.Generic;

class MyObj : IComparable<MyObj>
{
    public string Name { get; set; }
    public string ObjectID { get; set; }

    public int CompareTo(MyObj other)
    {
        if ((this.Name.CompareTo(other.Name) == 0) &&
            (this.ObjectID.CompareTo(other.ObjectID) == 0))
        {
            return 0;
        }
        return -1;
    }
}

2
最好实现IEquatable并重写EqualsGetHashCode,因为没有好的方法来判断哪个更大,只能判断它们是否相等。 - juharr
你是对的。忘记了IEquatable存在。我 == too_old。 - Itamar Kerbel

-1

我需要两个对象来进行审计日志记录,我编写了以下代码。对我来说它非常有效。

https://github.com/wbish/jsondiffpatch.net

public static bool DeepCompare(this object obj, object another)
    {   
            var diffObj = new JsonDiffPatch();
        
            if (ReferenceEquals(obj, another)) return true;
            if ((obj == null) || (another == null)) return false;
            if (obj.GetType() != another.GetType()) return false;

            var objJson = JsonConvert.SerializeObject(obj);
            var anotherJson = JsonConvert.SerializeObject(another);
            var result = diffObj.Diff(objJson, anotherJson);
            return result == null;
    }

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