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; } }
关键改动说明
- 路径追踪机制:新增了
currentPath和arrayPath参数,在递归调用时持续累积当前的层级路径,确保每个差异都能关联到完整的JSON路径。 - 路径拼接逻辑:
- 对象键路径用
->连接,比如batters拼接为batters->batter - 数组元素路径添加
[索引]后缀,比如batters->batter变为batters->batter[0]
- 对象键路径用
- 标准化差异输出:所有差异信息都以完整路径开头,后跟差异内容,完全匹配你期望的格式。
- 增强边界检查:新增了对目标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
相关产品推荐
相关产品推荐

