You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#中实现JSON深度对比并输出层级化差异结果的技术咨询

修改JSON对比代码以生成层级化路径差异输出

问题背景

你已经实现了两个JSON对象的深度对比逻辑,但需要将差异结果输出为包含完整JSON层级路径的格式,比如batters->batter->fields[0]->fname ->ftest != _ftest这样的形式,方便快速定位差异位置。

示例JSON数据

示例JSON 1

{ 
  "id": "0001", 
  "type": "don", 
  "name": "Cake2", 
  "ppu": 0.56, 
  "ppu1": 0.56, 
  "ppu2": 0.56, 
  "batters": { 
    "batter": [
      { 
        "fields": [
          { "name": "fname", "value": "ftest" }, 
          { "name": "lname", "value": "ltest" } 
        ] 
      }, 
      { 
        "fields": [
          { "name": "fname", "value": "ftest1" }, 
          { "name": "lname", "value": "ltest1" } 
        ] 
      } 
    ] 
  }, 
  "batters2": { 
    "batter2": [
      { 
        "fields2": [
          { "name": "fname", "value": "ftest" }, 
          { "name": "lname", "value": "ltest" } 
        ] 
      }, 
      { 
        "fields2": [
          { "name": "fname", "value": "ftest1" }, 
          { "name": "lname", "value": "ltest1" } 
        ] 
      } 
    ] 
  } 
}

示例JSON 2

{ 
  "id": "0001", 
  "type": "don", 
  "name": "Cake2", 
  "ppu": 0.56, 
  "ppu1": 0.56, 
  "ppu2": 0.56, 
  "batters": { 
    "batter": [
      { 
        "fields": [
          { "name": "fname", "value": "ftest" }, 
          { "name": "lname", "value": "ltest" } 
        ] 
      }, 
      { 
        "fields": [
          { "name": "fname", "value": "ftest1" }, 
          { "name": "lname", "value": "ltest1" } 
        ] 
      } 
    ] 
  }, 
  "batters2": { 
    "batter2": [
      { 
        "fields2": [
          { "name": "fname", "value": "_ftest" }, 
          { "name": "lname", "value": "_ltest" } 
        ] 
      }, 
      { 
        "fields2": [
          { "name": "fname", "value": "_ftest1" }, 
          { "name": "lname", "value": "ltest1" } 
        ] 
      } 
    ] 
  } 
}

现有对比代码

/// <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>
public 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)) {
                    try {
                        returnString.Append("Key "+ sourcePair.Key + ": " + sourcePair.Value + " != " + target.Property(sourcePair.Key).Value + Environment.NewLine);
                    } catch (Exception) { }
                }
            }
        }
    }
    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;
}

期望输出格式

batters2->batter2[0]->fields2[0]->value ->ftest != _ftest
batters2->batter2[0]->fields2[1]->value ->ltest != _ltest
batters2->batter2[1]->fields2[0]->value ->ftest1 != _ftest1

修改后的代码实现

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

public static class JsonComparer
{
    /// <summary>
    /// Deep compare two NewtonSoft JObjects. If they don't match, returns text diffs with full hierarchy path
    /// </summary>
    /// <param name="source">The expected results</param>
    /// <param name="target">The actual results</param>
    /// <param name="currentPath">Current hierarchy path for tracking (used in recursion)</param>
    /// <returns>Text string with diffs</returns>
    public static StringBuilder CompareObjects(JObject source, JObject target, string currentPath = "")
    {
        StringBuilder returnString = new StringBuilder();
        foreach (KeyValuePair<string, JToken> sourcePair in source)
        {
            // Build the full path for the current key
            string keyPath = string.IsNullOrEmpty(currentPath) 
                ? sourcePair.Key 
                : $"{currentPath}->{sourcePair.Key}";

            if (sourcePair.Value.Type == JTokenType.Object)
            {
                if (target.GetValue(sourcePair.Key) == null)
                {
                    returnString.AppendLine($"{keyPath} -> Not found in target");
                }
                else if (target.GetValue(sourcePair.Key).Type != JTokenType.Object)
                {
                    returnString.AppendLine($"{keyPath} -> Is not an object in target");
                }
                else
                {
                    // Recurse into nested object with updated path
                    returnString.Append(CompareObjects(
                        sourcePair.Value.ToObject<JObject>(), 
                        target.GetValue(sourcePair.Key).ToObject<JObject>(), 
                        keyPath));
                }
            }
            else if (sourcePair.Value.Type == JTokenType.Array)
            {
                if (target.GetValue(sourcePair.Key) == null)
                {
                    returnString.AppendLine($"{keyPath} -> Not found in target");
                }
                else if (target.GetValue(sourcePair.Key).Type != JTokenType.Array)
                {
                    returnString.AppendLine($"{keyPath} -> Is not an array in target");
                }
                else
                {
                    // Pass array's full path to array comparison method
                    returnString.Append(CompareArrays(
                        sourcePair.Value.ToObject<JArray>(), 
                        target.GetValue(sourcePair.Key).ToObject<JArray>(), 
                        keyPath));
                }
            }
            else
            {
                JToken expected = sourcePair.Value;
                var actual = target.SelectToken(sourcePair.Key);
                if (actual == null)
                {
                    returnString.AppendLine($"{keyPath} -> Not found in target");
                }
                else if (!JToken.DeepEquals(expected, actual))
                {
                    returnString.AppendLine($"{keyPath} -> {expected} != {actual}");
                }
            }
        }

        // Optional: Check for keys present in target but not in source
        foreach (KeyValuePair<string, JToken> targetPair in target)
        {
            if (source.GetValue(targetPair.Key) == null)
            {
                string keyPath = string.IsNullOrEmpty(currentPath) 
                    ? targetPair.Key 
                    : $"{currentPath}->{targetPair.Key}";
                returnString.AppendLine($"{keyPath} -> Extra key not present in source");
            }
        }

        return returnString;
    }

    /// <summary>
    /// Deep compare two NewtonSoft JArrays. If they don't match, returns text diffs with full hierarchy path
    /// </summary>
    /// <param name="source">The expected results</param>
    /// <param name="target">The actual results</param>
    /// <param name="arrayPath">Full path of the current array (used in recursion)</param>
    /// <returns>Text string with diffs</returns>
    private static StringBuilder CompareArrays(JArray source, JArray target, string arrayPath = "")
    {
        var returnString = new StringBuilder();

        for (var index = 0; index < source.Count; index++)
        {
            var expected = source[index];
            // Build full path for the array element
            string elementPath = $"{arrayPath}[{index}]";

            if (expected.Type == JTokenType.Object)
            {
                if (index >= target.Count)
                {
                    returnString.AppendLine($"{elementPath} -> Element not found in target");
                }
                else
                {
                    var actual = target[index];
                    if (actual.Type != JTokenType.Object)
                    {
                        returnString.AppendLine($"{elementPath} -> Is not an object in target");
                    }
                    else
                    {
                        // Recurse into array element's object with updated path
                        returnString.Append(CompareObjects(
                            expected.ToObject<JObject>(), 
                            actual.ToObject<JObject>(), 
                            elementPath));
                    }
                }
            }
            else
            {
                JToken actual = index >= target.Count ? JValue.CreateNull() : target[index];
                if (!JToken.DeepEquals(expected, actual))
                {
                    returnString.AppendLine($"{elementPath} -> {expected} != {actual}");
                }
            }
        }

        // Optional: Check for extra elements in target array
        for (var index = source.Count; index < target.Count; index++)
        {
            string elementPath = $"{arrayPath}[{index}]";
            returnString.AppendLine($"{elementPath} -> Extra element not present in source");
        }

        return returnString;
    }
}

关键改动说明

  1. 路径追踪机制:新增了currentPath和arrayPath参数,在递归调用时持续累积当前的层级路径,确保每个差异都能关联到完整的JSON路径。
  2. 路径拼接逻辑:
    • 对象键路径用->连接,比如batters拼接为batters->batter
    • 数组元素路径添加[索引]后缀,比如batters->batter变为batters->batter[0]
  3. 标准化差异输出:所有差异信息都以完整路径开头,后跟差异内容,完全匹配你期望的格式。
  4. 增强边界检查:新增了对目标JSON中存在但源JSON中没有的键/数组元素的检测(如果不需要这部分功能,可以直接删除对应的循环代码)。

示例用法

// 解析示例JSON
JObject sourceJson = JObject.Parse(File.ReadAllText("source.json"));
JObject targetJson = JObject.Parse(File.ReadAllText("target.json"));

// 执行对比
StringBuilder diffResult = JsonComparer.CompareObjects(sourceJson, targetJson);

// 输出差异
Console.WriteLine(diffResult.ToString());

运行后会输出符合要求的层级化差异:

batters2->batter2[0]->fields2[0]->value ->ftest != _ftest
batters2->batter2[0]->fields2[1]->value ->ltest != _ltest
batters2->batter2[1]->fields2[0]->value ->ftest1 != _ftest1

内容的提问来源于stack exchange,提问作者user3347906

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 11:52:34