【问题标题】:Find and return JSON differences using newtonsoft in C#?在 C# 中使用 newtonsoft 查找并返回 JSON 差异?
【发布时间】:2020-12-11 00:42:40
【问题描述】:

我想获取在使用 Newtonsoft 进行比较时不匹配的 JSON 部分的列表。

我有这段代码可以比较:

JObject xpctJSON = JObject.Parse(expectedJSON);
JObject actJSON = JObject.Parse(actualJSON);

bool res = JToken.DeepEquals(xpctJSON, actJSON);

但是找不到任何返回差异的东西。

【问题讨论】:

  • 这是一个很好的问题,很遗憾我没有一个很好的答案,但是这个问题和答案可能会对你有所帮助:stackoverflow.com/questions/630263/…
  • 我认为您的需求并不容易得到答复。例如,{a:{b:1,c:{d:3}}}{a:{b:1,c:{d:4}}} 之间的区别是什么,只有d?但是现在cs 有不同的值。所以它们也是不同的。如果cs 不同,那么as。与其这样,不如问问自己真正想做什么。
  • 另外,属性的顺序在 JSON 中可能不同,但仍代表等效的对象,例如{"a":"foo","b":"bar"}{"b":"bar","a":"foo"}。您是否希望将其视为差异?
  • 我也有同样的需求,记录 JSON 数据包的差异。我在想我会将每个键:值对减少到一个哈希表中。关键将是在树上一直带点的名称parent.child.property,这样每个都是唯一的,然后如果你对每个 json 图表都这样做,你就可以使用 Linq 快速比较并找到异常,并成为能够报告哪些是不同的。是不是想多了?

标签: c# json json.net


【解决方案1】:

只是为了帮助将来的查询。我遇到了一个不错的 json diff 工具。它完美地适用于 json 结构的差异/补丁:

jsondiffpatch.net 还有一个 nuget 包。

用法很简单。

var jdp = new JsonDiffPatch();
JToken diffResult = jdp.Diff(leftJson, rightJson);

【讨论】:

  • 真的很好,但是你不知道,在执行 JArrays diff 时是否有任何选项可以进行排序?还没有找到任何工具..
  • @tsul 我想一个选项可能是将 Json 反序列化为某个结构,对该结构中的数组进行排序,然后将其序列化为 json 并将其提供给 diff。
  • 谢谢,从那以后我找到了 Json.Comparer,它可以由自定义排序器和属性过滤器提供。我发现的另一个选项是反序列化为 Xml,然后使用 Microsoft.XmlDiffPatch。它甚至不需要任何自定义,只需使用 XmlDiffOptions.IgnoreChildOrder。
  • 这个包的问题是对象数组的差异不是很大,当使用'simple'选项时
【解决方案2】:

这是我写的递归版本。您使用两个 JObject 调用 CompareObjects,它会返回差异列表。您用两个 JArrays 调用 CompareArrays 并比较数组。数组和对象可以相互嵌套。

更新:@nttakr 在下面的评论中指出,这种方法实际上是一种偏差算法。它仅从源列表的角度告诉您不同之处。如果源中不存在键但目标列表中存在键,则将忽略该差异。这是为我的测试要求而设计的。这允许您测试您想要的项目,而无需在比较完成之前将它们从目标中删除。

    /// <summary>
    /// Deep compare two NewtonSoft JObjects. If they don't match, returns text diffs
    /// </summary>
    /// <param name="source">The expected results</param>
    /// <param name="target">The actual results</param>
    /// <returns>Text string</returns>

    private static StringBuilder CompareObjects(JObject source, JObject target)
    {
        StringBuilder returnString = new StringBuilder();
        foreach (KeyValuePair<string, JToken> sourcePair in source)
        {
            if (sourcePair.Value.Type == JTokenType.Object)
            {
                if (target.GetValue(sourcePair.Key) == null)
                {
                    returnString.Append("Key " + sourcePair.Key
                                        + " not found" + Environment.NewLine);
                }
                else if (target.GetValue(sourcePair.Key).Type != JTokenType.Object) {
                    returnString.Append("Key " + sourcePair.Key
                                        + " is not an object in target" + Environment.NewLine);
                }                    
                else
                {
                    returnString.Append(CompareObjects(sourcePair.Value.ToObject<JObject>(),
                        target.GetValue(sourcePair.Key).ToObject<JObject>()));
                }
            }
            else if (sourcePair.Value.Type == JTokenType.Array)
            {
                if (target.GetValue(sourcePair.Key) == null)
                {
                    returnString.Append("Key " + sourcePair.Key
                                        + " not found" + Environment.NewLine);
                }
                else
                {
                    returnString.Append(CompareArrays(sourcePair.Value.ToObject<JArray>(),
                        target.GetValue(sourcePair.Key).ToObject<JArray>(), sourcePair.Key));
                }
            }
            else
            {
                JToken expected = sourcePair.Value;
                var actual = target.SelectToken(sourcePair.Key);
                if (actual == null)
                {
                    returnString.Append("Key " + sourcePair.Key
                                        + " not found" + Environment.NewLine);
                }
                else
                {
                    if (!JToken.DeepEquals(expected, actual))
                    {
                        returnString.Append("Key " + sourcePair.Key + ": "
                                            + sourcePair.Value + " !=  "
                                            + target.Property(sourcePair.Key).Value
                                            + Environment.NewLine);
                    }
                }
            }
        }
        return returnString;
    }

    /// <summary>
    /// Deep compare two NewtonSoft JArrays. If they don't match, returns text diffs
    /// </summary>
    /// <param name="source">The expected results</param>
    /// <param name="target">The actual results</param>
    /// <param name="arrayName">The name of the array to use in the text diff</param>
    /// <returns>Text string</returns>

    private static StringBuilder CompareArrays(JArray source, JArray target, string arrayName = "")
    {
        var returnString = new StringBuilder();
        for (var index = 0; index < source.Count; index++)
        {

            var expected = source[index];
            if (expected.Type == JTokenType.Object)
            {
                var actual = (index >= target.Count) ? new JObject() : target[index];
                returnString.Append(CompareObjects(expected.ToObject<JObject>(),
                    actual.ToObject<JObject>()));
            }
            else
            {

                var actual = (index >= target.Count) ? "" : target[index];
                if (!JToken.DeepEquals(expected, actual))
                {
                    if (String.IsNullOrEmpty(arrayName))
                    {
                        returnString.Append("Index " + index + ": " + expected
                                            + " != " + actual + Environment.NewLine);
                    }
                    else
                    {
                        returnString.Append("Key " + arrayName
                                            + "[" + index + "]: " + expected
                                            + " != " + actual + Environment.NewLine);
                    }
                }
            }
        }
        return returnString;
    }

【讨论】:

  • 您的算法不完整,缺少差异。如果 Target 具有“更多”属性或“更多”数组项而不是源,您的算法会将源和目标标记为 EQUAL,但不是。
  • 是的,这实际上是设计使然。被测系统有时具有我们不关心的额外属性。此代码允许我检查我关心的项目并忽略我不关心的项目,方法是将它们从源列表中删除。在 .NET/C# 中,额外的项目通常不是问题。我听说有基于 Java 的系统在发生这种情况时会崩溃(但我没有测试这些。
  • 如果源是对象但目标是值,我认为CompareObjects 中可能存在错误。我想你可能需要else if (target.GetValue(sourcePair.Key).Type != JTokenType.Object)。在if (sourcePair.Value.Type == JTokenType.Object) 语句中。感谢代码 sn-p 顺便说一句。真的很有用!
  • 我认为 CompareArrays 方法可能会被改进以比较数组的数组。
  • 我们的数据模型没有数组数组。它们要么具有基元数组,要么具有对象数组(可能包括数组)。包含第二个 else if 子句应该很简单,该子句递归调用 CompareArrays 来检查子数组。
【解决方案3】:

我的解决方案是基于之前答案的想法:

public static JObject FindDiff(this JToken Current, JToken Model)
{
    var diff = new JObject();
    if (JToken.DeepEquals(Current, Model)) return diff;

    switch(Current.Type)
    {
        case JTokenType.Object:
            {
                var current = Current as JObject;
                var model = Model as JObject;
                var addedKeys = current.Properties().Select(c => c.Name).Except(model.Properties().Select(c => c.Name));
                var removedKeys = model.Properties().Select(c => c.Name).Except(current.Properties().Select(c => c.Name));
                var unchangedKeys = current.Properties().Where(c => JToken.DeepEquals(c.Value, Model[c.Name])).Select(c => c.Name);
                foreach (var k in addedKeys)
                {
                    diff[k] = new JObject
                    {
                        ["+"] = Current[k]
                    };
                }
                foreach (var k in removedKeys)
                {
                    diff[k] = new JObject
                    {
                        ["-"] = Model[k]
                    };
                }
                var potentiallyModifiedKeys = current.Properties().Select(c => c.Name).Except(addedKeys).Except(unchangedKeys);
                foreach (var k in potentiallyModifiedKeys)
                {
                    var foundDiff = FindDiff(current[k], model[k]);
                    if(foundDiff.HasValues) diff[k] = foundDiff;
                }
            }
            break;
        case JTokenType.Array:
            {
                var current = Current as JArray;
                var model = Model as JArray;
                var plus = new JArray(current.Except(model, new JTokenEqualityComparer()));
                var minus = new JArray(model.Except(current, new JTokenEqualityComparer()));
                if (plus.HasValues) diff["+"] = plus;
                if (minus.HasValues) diff["-"] = minus;
            }
            break;
        default:
            diff["+"] = Current;
            diff["-"] = Model;
            break;
    }

    return diff;
}

【讨论】:

  • 如果与@Pravin 解决方案中的示例 json 一起使用,您的扩展方法比 JsonDiffPatch.net 快 10 倍。
  • 修改为使用 JTokenEqualityComparer 进行数组比较。没有它,我看到数组差异表示为完整的数组元素替换。
  • 这真的很漂亮。我对其进行了调整,以将差异写入包含对象路径的 POCO,它是我正在处理的漂移报告的完美格式。谢谢你!
  • 值得注意的是,在属性是对象数组的情况下,即使在该对象上具有单个属性级别差异,它也会给出整个对象。我确定这是为了保持通用性。做更多的事情需要通过标识符属性对齐数组,或者如果你真的想疯狂地创建一个差异/匹配来通用地做到这一点。
  • @BrianS 你有一个例子说明你是如何解决两个数组之间的比较的吗?
【解决方案4】:

这是一个相对较老的问题,但发布了解决此问题的一种可能方法,假设您想要的结果正是更改了哪些属性值

   string sourceJsonString = "{'name':'John Doe','age':'25','hitcount':34}";
   string targetJsonString = "{'name':'John Doe','age':'26','hitcount':30}";

   JObject sourceJObject = JsonConvert.DeserializeObject<JObject>(sourceJsonString);
   JObject targetJObject = JsonConvert.DeserializeObject<JObject>(targetJsonString);

   if (!JToken.DeepEquals(sourceJObject, targetJObject))
   {
     foreach (KeyValuePair<string, JToken> sourceProperty in sourceJObject)
     {
         JProperty targetProp = targetJObject.Property(sourceProperty.Key);

          if (!JToken.DeepEquals(sourceProperty.Value, targetProp.Value))
          {
              Console.WriteLine(string.Format("{0} property value is changed", sourceProperty.Key));
          }
          else
          {
              Console.WriteLine(string.Format("{0} property value didn't change", sourceProperty.Key));
          }
      }
   }
   else
   {
      Console.WriteLine("Objects are same");
   }  

注意:这尚未针对非常深的层次结构进行测试。

【讨论】:

  • 谢谢,我将它转换为递归函数,并用它来比较对象和子对象,一直到基本类型。
  • 嗨@Walter,你能发布你的递归解决方案吗?
  • 代码不太适合 cmets,因此已将其添加为答案。 @bboyle1234 如果您觉得有用,请点赞。
【解决方案5】:

注意以下库:

using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

我不完全确定我是否正确理解了您的问题。 我假设您正在尝试确定实际 JSON 中缺少哪些键。

如果您只是对缺少的 KEYS 感兴趣,下面的代码将对您有所帮助,如果没有,请提供您尝试识别的差异类型的示例。

  public IEnumerable<JProperty> DoCompare(string expectedJSON, string actualJSON)
    {
        // convert JSON to object
        JObject xptJson = JObject.Parse(expectedJSON);
        JObject actualJson = JObject.Parse(actualJSON);

        // read properties
        var xptProps = xptJson.Properties().ToList();
        var actProps = actualJson.Properties().ToList();

        // find missing properties
        var missingProps = xptProps.Where(expected => actProps.Where(actual => actual.Name == expected.Name).Count() == 0);

        return missingProps;
    }

请注意,如果此方法返回空 IEnumerable,则 ACTUAL JSON 具有根据预期 JSON 的结构所需的所有键。

注意:实际的 JSON 可能仍具有预期 JSON 不需要的更多键。

进一步解释我的笔记...

假设您预期的 JSON 是:

{ Id: 1, Name: "Item One", Value: "Sample" }

并且您的实际 JSON 是:

{ Id: 1, Name: "Item One", SomeProp: "x" }

上面的函数会告诉你缺少 Value 键,但不会提及 SomeProp 键的任何内容......除非你交换输入参数。

【讨论】:

  • 算法不正确。它只检查actualJSON 是否包含expectedJSON 的所有属性。那些在实际 JSON 中不存在于期望 JSON 中的属性呢?这也是一个“差异”,但您的代码将找不到它。属性的更改值也有资格作为差异。您的代码将完全忽略这一点。
【解决方案6】:

这是一个非常古老的线程,但是由于几个月前我来这里寻找可靠的工具但找不到,我已经编写了自己的,如果你正在寻找类似的东西,你可以使用它以下:

JSON 1

{
  "name":"John",
  "age":30,
  "cars": {
    "car1":"Ford",
    "car2":"BMW",
    "car3":"Fiat"
  }
 }

JSON 2

{
  "name":"John",
  "cars": {
    "car1":"Ford",
    "car2":"BMW",
    "car3":"Audi",
    "car4":"Jaguar"
  }
 }

用法


 var j1 = JToken.Parse(Read(json1));
 var j2 = JToken.Parse(Read(json2));

 var diff = JsonDifferentiator.Differentiate(j1,j2);

结果

{
  "-age": 30,
  "*cars": {
    "*car3": "Fiat",
    "+car4": "Jaguar"
  }
}

随时查看源代码并查看测试,欢迎您的反馈:)

https://www.nuget.org/packages/JsonDiffer

【讨论】:

  • 很棒的库阿明!刚刚在我们正在制作原型的测试工具中充分利用了它。
  • @MoscaPt 很高兴听到它有帮助! :)
【解决方案7】:

这里没有一个答案是我所需要的。

这是一个方法,它为比较的两个对象中的每一个返回一个 JObject。 JObject 仅包含不同的属性。这对于扫描对实体的更改并存储前后快照(序列化 JObject)很有用。

注意:更改扫描仅发生在顶级属性上。

        private Tuple<JObject, JObject> GetDeltaState<TRead>(TRead before, TRead after)
    {
        if (before == null && after == null)
            return new Tuple<JObject, JObject>(null, null);

        JObject beforeResult;
        JObject afterResult;

        // If one record is null then we don't need to scan for changes
        if (before == null ^ after == null)
        {
            beforeResult = before == null ? null : JObject.FromObject(before, _jsonSerializer);
            afterResult = after == null ? null : JObject.FromObject(after, _jsonSerializer);

            return new Tuple<JObject, JObject>(beforeResult, afterResult);
        }

        beforeResult = new JObject();
        afterResult = new JObject();

        JObject beforeState = JObject.FromObject(before, _jsonSerializer);
        JObject afterState = JObject.FromObject(after, _jsonSerializer);

        // Get unique properties from each object
        IEnumerable<JProperty> properties = beforeState.Properties().Concat(afterState.Properties()).DistinctBy(x => x.Name);

        foreach (JProperty prop in properties)
        {
            JToken beforeValue = beforeState[prop.Name];
            JToken afterValue = afterState[prop.Name];

            if (JToken.DeepEquals(beforeValue, afterValue))
                continue;

            beforeResult.Add(prop.Name, beforeValue);
            afterResult.Add(prop.Name, afterValue);
        }

        return new Tuple<JObject, JObject>(beforeResult, afterResult);
    }

【讨论】:

    【解决方案8】:

    我已经转换为更准确的对象数组

    public static JObject FindDiff(this JToken leftJson, JToken rightJson)
    {
        var difference = new JObject();
        if (JToken.DeepEquals(leftJson, rightJson)) return difference;
    
        switch (leftJson.Type) {
            case JTokenType.Object:
                {
                    var LeftJSON = leftJson as JObject;
                    var RightJSON = rightJson as JObject;
                    var RemovedTags = LeftJSON.Properties().Select(c => c.Name).Except(RightJSON.Properties().Select(c => c.Name));
                    var AddedTags = RightJSON.Properties().Select(c => c.Name).Except(LeftJSON.Properties().Select(c => c.Name));
                    var UnchangedTags = LeftJSON.Properties().Where(c => JToken.DeepEquals(c.Value, RightJSON[c.Name])).Select(c => c.Name);
                    foreach(var tag in RemovedTags)
                    {
                        difference[tag] = new JObject
                        {
                            ["-"] = LeftJSON[tag]
                        };
                    }
                    foreach(var tag in AddedTags)
                    {
                        difference[tag] = new JObject
                        {
                            ["-"] = RightJSON[tag]
                        };
                    }
                    var ModifiedTags = LeftJSON.Properties().Select(c => c.Name).Except(AddedTags).Except(UnchangedTags);
                    foreach(var tag in ModifiedTags)
                    {
                        var foundDifference = Compare(LeftJSON[tag], RightJSON[tag]);
                        if (foundDifference.HasValues) {
                            difference[tag] = foundDifference;
                        }
                    }
                }
                break;
            case JTokenType.Array:
                {
                    var LeftArray = leftJson as JArray;
                    var RightArray = rightJson as JArray;
    
                    if (LeftArray != null && RightArray != null) {
                        if (LeftArray.Count() == RightArray.Count()) {
                            for (int index = 0; index < LeftArray.Count(); index++)
                            {
                                var foundDifference = Compare(LeftArray[index], RightArray[index]);
                                if (foundDifference.HasValues) {
                                    difference[$"{index}"] = foundDifference;
                                }
                            }
                        }
                        else {
                            var left = new JArray(LeftArray.Except(RightArray, new JTokenEqualityComparer()));
                            var right = new JArray(RightArray.Except(LeftArray, new JTokenEqualityComparer()));
                            if (left.HasValues) {
                                difference["-"] = left;
                            }
                            if (right.HasValues) {
                                difference["+"] = right;
                            }
                        }
                    }
                }
                break;
            default:
                difference["-"] = leftJson;
                difference["+"] = rightJson;
                break;
        }
    
        return difference;
    }
    

    【讨论】:

    【解决方案9】:
        public static void Validate(JToken actual, JToken expected, IList<string> diffMessages)
        {
            if (actual == null && expected == null)
            {
                // handle accroding to requirement
                return;
            }
    
            if (actual == null)
            {
                diffMessages.Add($"Diff on {expected.Path}: actual - null, expected - {expected}");
                return;
            }
    
            if (expected == null)
            {
                diffMessages.Add($"Diff on {actual.Path}: actual - {actual}, expected - null");
                return;
            }
    
            if (actual.Type != JTokenType.Object && actual.Type != JTokenType.Array && actual.Type != JTokenType.Property)
            {
                if (!JToken.DeepEquals(actual, expected))
                {
                    diffMessages.Add($"Diff on {actual.Path}: actual- {actual}, expected - {expected}");
                }
    
                return;
            }
    
    
            // recursion
    
            foreach (var jItem in actual)
            {
                var newExpected = expected.Root.SelectToken(jItem.Path);
                Validate(jItem, newExpected, diffMessages);
            }
    
        }
    

    【讨论】:

    • 虽然此代码可能会解决问题,including an explanation 关于如何以及为什么解决问题将真正有助于提高您的帖子质量,并可能导致更多的赞成票。请记住,您正在为将来的读者回答问题,而不仅仅是现在提问的人。请编辑您的答案以添加解释并说明适用的限制和假设。
    猜你喜欢
    • 1970-01-01
    • 2021-08-30
    • 1970-01-01
    • 1970-01-01
    • 2020-01-21
    • 1970-01-01
    • 2017-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多