内容简介:工作上有一堆重複性很高的網頁表單欄位處理需求,我想做一個萬用框架搞定它,其中有個欄位映對問題。C# 端的 ViewModel 包含巢狀階層結構,屬性與陣列交雜,映對到 HTML 端 INPUT、SELECT、TEXTAREA 時,我想為所有屬性唯一識別 ID。例如,ViewModel 的 A 屬性是個物件陣列,因此 A 陣列的第二個元素物件的 B 屬性,就可寫成 Model.A[1].B。例如這樣的 JSON 結構:
工作上有一堆重複性很高的網頁表單欄位處理需求,我想做一個萬用框架搞定它,其中有個欄位映對問題。
C# 端的 ViewModel 包含巢狀階層結構,屬性與陣列交雜,映對到 HTML 端 INPUT、SELECT、TEXTAREA 時,我想為所有屬性唯一識別 ID。例如,ViewModel 的 A 屬性是個物件陣列,因此 A 陣列的第二個元素物件的 B 屬性,就可寫成 Model.A[1].B。
例如這樣的 JSON 結構:
{ "A": [1,2,3], "B": {"X": 4,"Y": 5}, "C": [ { "Z": 6, "D": [ { "P": true, "Q": false, "B": {"X": 0,"Y": 1} }, { "P": true, "Q": true, "B": null } ] }, { "Z": 7, "D": null } ] }
要轉成這樣的 Dictionary<string, object>:
{ "A[0]": 1, "A[1]": 2, "A[2]": 3, "B.X": 4, "B.Y": 5, "C[0].Z": 6, "C[0].D[0].P": true, "C[0].D[0].Q": false, "C[0].D[0].B.X": 0, "C[0].D[0].B.Y": 1, "C[0].D[1].P": true, "C[0].D[1].Q": true, "C[0].D[1].B": null, "C[1].Z": 7, "C[1].D": null }
好久沒遇到這種有點難又不會太難的挑戰,當成程式魔人的 Coding4Fun 假日暖身操剛好,順手分享到Facebook 專頁,得到來自讀者朋友們的迴響,我學到幾件事:
- 這議題不算罕見,有些 工具 只吃 Key / Value 或 Flat JSON,就需要類似轉換
- 爬文關鍵字是 Flatten JSON,Nested to Flat... 等,可找到很多參考資料
- 它算是 DFS (Deep First Search) Traversal 的經典應用
- 網路上現成程式範例還不少,但幾乎清一色都是 JavaScript。JavaScript 的弱型別跟動態物件特性(obj[propName]=propValue),在此佔盡便宜
- 我沒找到 C# 寫的範例。很好,就不再花時間找了,剛好是練功止手癢的好理由(謎之聲:是有多愛寫程式啦?)
我沒學過 DFS,但腦海大概知道怎麼用 Reflection + Recursive 實現,不到 30 分鐘就寫完列序化轉換,但在還原部分陷入苦戰。
C# 不像 JavaScript 可以 object[propNameA][subPropName][2] = propValue
,甚至用 eval() 動態組程式碼克服刁鑽情境。前後試過多種寫法,試過 Json.NET 客製轉換、先對映成 TreeNode 結構再轉、串接 Dictionary<string, object>、借用 JObject... 砍掉重練多次不是失敗就是程式碼複雜到自己想吐。
最後我決定回歸初心,既然用 JavaScript 動態物件我有把握搞定,何不在 C# 也用動態物件解決? 於是我想起 ExpandoObject! (參考: 既然要動態就動個痛快 - ExpandoObject )
前後耗費六個小時(好久沒有想程式想這麼久了),第一個可用版本出爐,轉換邏輯大約一百行左右:
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; namespace FlatJson { /// <summary> /// Flatten nested JSON /// { "A":[1,2,3], "B":{"X":4,"Y":5},"C":[{"Z":6},{"Z":7}]} /// to simple key/value dictionary /// {"A[0]":1,"A[1]":2,"A[2]": 3,"B.X":4,"B.Y":5,"C[0].Z":6,"C[1].Z":7} /// </summary> public static class FlatJsonConvert { static void ExploreAddProps(Dictionary<string, object> props, string name, Type type, object value) { if (type.IsPrimitive || value == null) props.Add(name, value); else if (type.IsArray) { var a = (Array)value; for (var i = 0; i < a.Length; i++) ExploreAddProps(props, $"{name}[{i}]", type.GetElementType(), a.GetValue(i)); } else { type.GetProperties().ToList() .ForEach(p => { var prefix = string.IsNullOrEmpty(name) ? string.Empty : name + "."; ExploreAddProps(props, $"{prefix}{p.Name}", p.PropertyType, p.GetValue(value)); }); } } public static string Serialize<T>(T data) { var props = new Dictionary<string, object>(); ExploreAddProps(props, string.Empty, typeof(T), data); return JsonConvert.SerializeObject(props, Formatting.Indented); } public static T Deserialize<T>(string json) { var props = JsonConvert.DeserializeObject<Dictionary<string, object>>(json); ExpandoObject data = new ExpandoObject(); Action<string, object> SetProp = (propName, propValue) => { var seg = propName.Split('.'); object curr = data; for (var i = 0; i < seg.Length; i++) { var n = seg[i]; var isLeaf = i == seg.Length - 1; if (n.Contains("[")) //for array { var pn = n.Split('[').First(); var d = curr as IDictionary<string, object>; if (!d.ContainsKey(pn)) d[pn] = new List<object>(); if (isLeaf) (d[pn] as List<object>).Add(propValue); else { var idx = int.Parse(n.Split('[').Last().TrimEnd(']')); var lst = (d[pn] as List<object>); if (idx == lst.Count) lst.Add(new ExpandoObject()); if (idx < lst.Count) curr = lst[idx]; else throw new NotImplementedException("Skiped index is not supported"); } } else //for property { if (curr is List<object>) throw new NotImplementedException("Array of array is not supported"); else { var d = curr as IDictionary<string, object>; if (isLeaf) d[n] = propValue; else { if (!d.ContainsKey(n)) d[n] = new ExpandoObject(); curr = d[n]; } } } } }; props.Keys.OrderBy(o => o.Split('.').Length) //upper level first .ThenBy(o => //prop first, array elements ordered by index !o.Split('.').Last().Contains("]") ? -1 : int.Parse(o.Split('.').Last().Split('[').Last().TrimEnd(']'))) .ToList().ForEach(o => SetProp(o, props[o])); var unflattenJson = JsonConvert.SerializeObject(data); return JsonConvert.DeserializeObject<T>(unflattenJson); } } }
驗證程式如下:
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FlatJson { class Program { static void Main(string[] args) { AType data = new AType() { A = new int[] { 1, 2, 3 }, B = new BType() { X = 4, Y = 5 }, C = new CType[] { new CType() { Z = 6, D = new DType[] { new DType() { P = true, Q = false, B = new BType() { X = 0, Y = 1 } }, new DType() { P = true, Q = true } } }, new CType() { Z = 7} } }; var origJson = JsonConvert.SerializeObject(data, Formatting.Indented); var flattenJson = FlatJsonConvert.Serialize<AType>(data); var restored = FlatJsonConvert.Deserialize<AType>(flattenJson); var restoredJson = JsonConvert.SerializeObject(restored, Formatting.Indented); Console.WriteLine(origJson); Console.WriteLine(flattenJson); Console.WriteLine("Check Result = " + (restoredJson == origJson ? "PASS" : "FAIL")); Console.Read(); } } public class AType { public int[] A { get; set; } public BType B { get; set; } public CType[] C { get; set; } } public class BType { public int X { get; set; } public int Y { get; set; } } public class CType { public int Z { get; set; } public DType[] D { get; set; } } public class DType { public bool P { get; set; } public bool Q { get; set; } public BType B { get; set; } } }
實測過關:
這個演算法有個限制,它假設陣列元素必須是基本型別或其他類別,不能直接是陣列。此外,它跑過的測試案例有限,對更複雜的情境可能存在 Bug,但已符合我眼前要處理的需求。
關鍵元件完成,通過自己設定的小小挑戰,心滿意足~
A implementation of C# to flatten nested JSON structure to key/value dictionary.
以上所述就是小编给大家介绍的《Coding4Fun-巢狀 JSON 資料結構與 Key/Value 雙向轉換(C# 版)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。