看文章標題就知道,本文的主題就是關於JSON,JSON轉換器(JsonConverter)具備將C#定義的類源代碼直接轉換成對應的JSON字符串,以及將JSON字符串轉換成對應的C#定義的類源代碼,而JSON操做技巧則說明如何經過JPath來快速的定位JSON的屬性節點從而達到靈活讀寫JSON目的。html
如今都流行微服務,先後端分離,而微服務之間、先後端之間數據交互更多的是基於REST FUL風格的API,API的請求與響應通常經常使用格式都是JSON。當編寫了一些API後,爲了可以清楚的描述API的請求及響應數據格式(即:JSON格式),以便提供給API服務的消費者(其它微服務、前端)開發人員進行對接開發,一般是編寫API說明文檔,說明文檔中通常包含入參JSON格式說明以及響應的JSON格式說明示例,但若是API涉及數目較多,全由開發人員人工編寫,那效率就很是低下,並且不必定準確。因而就有了Swagger,在API項目中集成swagger組件,就會由swagger根據API的ACTION方法定義及註解生成標準的在線API說明文檔,具體用法請參見網上相關文章說明。固然除了swagger還有其它相似的集成式的生成在線API說明文檔,你們有興趣的話能夠去網上找找資源。雖然說swagger組件確實解放了開發人員的雙手,無需人工編寫就自動生成在線API文檔,但我認爲仍是有一些不足,或者說是不太方便的地方:一是必需集成到API項目中,與API項目自己有耦合與依賴,沒法單獨做爲API幫助文檔項目,在有些狀況下可能並不想依賴swagger,不想時刻把swagger生成API文檔暴露出來;二是目前都是生成的在線API文檔,若是API在某些網絡環境下不可訪問(好比:受限),那在線的API文檔基本等同於沒用,雖然說swagger也能夠經過複雜的配置或改造支持導出離線的API文檔,但總歸是有必定的學習成本。那有沒有什麼替代方案能解決swagger相似的在線API文檔的不足,又避免人工低效編寫的情況呢?可能有,我(夢在旅途)沒了解過,但我爲了解決上述問題,基於.NET動態編譯&Newtonsoft.Json封裝實現了一個JSON轉換器(JsonConverter),採用人工編寫+JSON自動生成的方式來實現靈活、快速、離線編寫API說明文檔。前端
先來看一下JsonConverter工具的界面吧,以下圖示:node
工具界面很簡單,下面簡要說明一下操做方法:git
class類源代碼轉換成Json字符串:先將項目中定義的class類源代碼複製粘貼到Class Code文本框區域【注意:如有繼承或屬性自己又是另外一個類,則相關的class類定義源代碼均應一同複製,using合併,namespace容許多個,目的是確保能夠動態編譯經過】,而後點擊上方的【Parse】按鈕,以便執行動態編譯並解析出Class Code文本框區域中所包含的class Type,最後選擇須要生成JSON的class Type,點擊中間的【To Json】按鈕,便可將選擇的class Type 序列化生成JSON字符串並展現在右邊的Json String文本框中;github
示例效果以下圖示:(支持繼承,複雜屬性)編程
有了這個功能之後,API寫好後,只須要把ACTION方法的入參class源代碼複製過來而後進行class to JSON轉換便可快速生成入參JSON,不管是本身測試仍是寫文檔都很方便。建議使用markdown語法來編寫API文檔。json
Json字符串轉換成class類定義源代碼:先將正確的JSON字符串複製粘貼到Json String文本框中,而後直接點擊中間的【To Class】按鈕,彈出輸入要生成的class名對話框,輸入後點擊肯定就執行轉換邏輯,最終將轉換成功的class定義源代碼展現在左邊的Class Code文本框區域中;後端
示例效果以下圖示:(支持複雜屬性,可以遞歸生成JSON所需的子類,相似以下的Address,注意暫不支持數組嵌套數組這種很是規的格式,即:[ [1,2,3],[4,5,6] ])數組
JsonConverter工具實現原理及代碼說明:markdown
class Code To Json 先利用.NET動態編譯程序集的方式,把class Code動態編譯成一個內存的臨時程序集Assembly,而後得到該Assembly中的Class Type,最後經過反射建立一個Class Type空實例,再使用Newtonsoft.Json 序列化成JSON字符串便可。
動態編譯是:Parse,序列化是:ToJsonString,須要關注的點是:動態編譯時,須要引用相關的.NET運行時DLL,而這些DLL必需在工具的根目錄下,不然可能致使引用找不到DLL致使編譯失敗,故項目中引用了常見的幾個DLL,並設置了複製到輸出目錄中,若是後續有用到其它特殊的類型一樣參照該方法先把DLL包含到項目中,並設置複製到輸出目錄中,而後在動態編譯代碼中使用cp.ReferencedAssemblies.Add("XXXX.dll");進行添加。核心代碼以下:
private List<string> Parse(string csCode) { var provider = new CSharpCodeProvider(); var cp = new CompilerParameters(); cp.GenerateExecutable = false; cp.GenerateInMemory = true; cp.IncludeDebugInformation = false; //cp.ReferencedAssemblies.Add("mscorlib.dll"); cp.ReferencedAssemblies.Add("System.dll"); cp.ReferencedAssemblies.Add("System.Data.dll"); cp.ReferencedAssemblies.Add("System.Linq.dll"); cp.ReferencedAssemblies.Add("System.ComponentModel.DataAnnotations.dll"); cp.ReferencedAssemblies.Add("Newtonsoft.Json.dll"); CompilerResults result = provider.CompileAssemblyFromSource(cp, csCode); List<string> errList = new List<string>(); if (result.Errors.Count > 0) { foreach (CompilerError err in result.Errors) { errList.Add(string.Format("Line:{0},ErrorNumber:{1},ErrorText:{2}", err.Line, err.ErrorNumber, err.ErrorText)); } MessageBox.Show("Compile error:\n" + string.Join("\n", errList)); return null; } dyAssembly = result.CompiledAssembly; return dyAssembly.GetTypes().Select(t => t.FullName).ToList(); } private string ToJsonString(string targetType) { if (dyAssembly == null) { MessageBox.Show("dyAssembly is null!"); return null; } var type = dyAssembly.GetType(targetType); var typeConstructor = type.GetConstructor(Type.EmptyTypes); var obj = typeConstructor.Invoke(null); return JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings { DateFormatString = "yyyy-MM-dd HH:mm:ss" }); }
Json to Class code 先使用JObject.Parse將json字符串轉換爲通用的JSON類型實例,而後直接經過獲取全部JSON屬性集合並遍歷這些屬性,經過判斷屬性節點的類型,如果子JSON類型【即:JObject】則建立對象屬性字符串 同時遞歸查找子對象,如果數組類型【即:JArray】則建立List集合屬性字符串,同時進一步判斷數組的元素類型,如果子JSON類型【即:JObject】則還是遞歸查找子對象,最終拼接成全部類及其子類的class定義源代碼字符串。核心代碼以下:
private string ToClassCode(JObject jObject, string className) { var classCodes = new Dictionary<string, string>(); classCodes.Add(className, BuildClassCode(jObject, className, classCodes)); StringBuilder codeBuidler = new StringBuilder(); foreach (var code in classCodes) { codeBuidler.AppendLine(code.Value); } return codeBuidler.ToString(); } private Dictionary<JTokenType, string> jTokenBaseTypeMappings = new Dictionary<JTokenType, string> { { JTokenType.Integer,"int" },{ JTokenType.Date,"DateTime" },{ JTokenType.Bytes,"byte[]"},{ JTokenType.Boolean,"bool"},{ JTokenType.String,"string"}, { JTokenType.Null,"object"},{ JTokenType.Float,"float"},{ JTokenType.TimeSpan,"long"} }; private string BuildClassCode(JObject jObject, string className, Dictionary<string, string> classCodes) { StringBuilder classBuidler = new StringBuilder(); classBuidler.Append("public class " + className + " \r\n { \r\n"); foreach (var jProp in jObject.Properties()) { string propClassName = "object"; if (jProp.Value.Type == JTokenType.Object) { if (jProp.Value.HasValues) { propClassName = GetClassName(jProp.Name); if (classCodes.ContainsKey(propClassName)) { propClassName = className + propClassName; } classCodes.Add(propClassName, BuildClassCode((JObject)jProp.Value, propClassName, classCodes)); } classBuidler.AppendFormat("public {0} {1} {2}\r\n", propClassName, jProp.Name, "{get;set;}"); } else if (jProp.Value.Type == JTokenType.Array) { if (jProp.Value.HasValues) { var jPropArrItem = jProp.Value.First; if (jPropArrItem.Type == JTokenType.Object) { propClassName = GetClassName(jProp.Name); if (classCodes.ContainsKey(propClassName)) { propClassName = className + propClassName; } propClassName += "Item"; classCodes.Add(propClassName, BuildClassCode((JObject)jPropArrItem, propClassName, classCodes)); } else { if (jTokenBaseTypeMappings.ContainsKey(jPropArrItem.Type)) { propClassName = jTokenBaseTypeMappings[jPropArrItem.Type]; } else { propClassName = jPropArrItem.Type.ToString(); } } } classBuidler.AppendFormat("public List<{0}> {1} {2}\r\n", propClassName, jProp.Name, "{get;set;}"); } else { if (jTokenBaseTypeMappings.ContainsKey(jProp.Value.Type)) { propClassName = jTokenBaseTypeMappings[jProp.Value.Type]; } else { propClassName = jProp.Value.Type.ToString(); } classBuidler.AppendFormat("public {0} {1} {2} \r\n", propClassName, jProp.Name, "{get;set;}"); } } classBuidler.Append("\r\n } \r\n"); return classBuidler.ToString(); }
把JSON字符串轉換爲class類源代碼,除了我這個工具外,網上也有一些在線的轉換網頁可使用,另外我再分享一個小技巧,即:直接利用VS的編輯-》【選擇性粘貼】,而後選擇粘貼成JSON類或XML便可,菜單位置:
經過這種粘貼到JSON與個人這個工具的效果基本相同,只是多種選擇而矣。
JsonConverter工具已開源並上傳至GitHub,地址:https://github.com/zuowj/JsonConverter
下面再講講JSON數據的讀寫操做技巧。
通常操做JSON,大多要麼是把class類的實例數據序列化成JSON字符串,以便進行網絡傳輸,要麼是把JSON字符串反序列化成class類的數據實例,以即可以在程序獲取這些數據。然而其實還有一些不經常使用的場景,也是與JSON有關,詳見以下說明。
場景一:若是已有JSON字符串,如今須要得到指定屬性節點的數據,且指定的屬性名不肯定,由外部傳入或邏輯計算出來的【即:不能直接在代碼中寫死要獲取的屬性邏輯】,那麼這時該如何快速的按需獲取任意JSON節點的數據呢?
常規解決方案:先反序列化成某個class的實例對象(或JObject實例對象),而後經過反射獲取屬性,並經過遞歸及比對屬性名找出最終的屬性,最後返回該屬性的值。
場景二:若是已有某個class實例對象數據,如今須要動態更改指定屬性節點的數據【即動態給某個屬性賦值】,該如何操做呢?
常規解決方案:經過反射獲取屬性,並經過遞歸及比對屬性名找出最終的屬性,最後經過反射給該屬性設置值。
場景三:若是已有JSON字符串,如今須要動態添加新屬性節點,該屬性節點能夠是任意嵌套子對象的屬性節點中,該如何操做呢?
常規解決方案:先反序列化成JObject實例對象,而後遞歸查找目標位置,最後在指定的位置建立新的屬性節點。
三種場景概括一下其實就是須要對JSON的某個屬性節點數據能夠快速動態的增、改、刪、查操做,然而常規則解決方案基本上都是須要靠遞歸+反射+比對,運行性能可想而知,而我今天分享的JSON操做技巧就是解決上述問題的。
重點來了,咱們能夠經過JPath表達式來快速定位查找JSON的屬性節點,就像XML利用XPath同樣查找DOM節點。
JPath表達式是什麼呢? 詳見:https://goessner.net/articles/JsonPath/ ,Xpath與JSONPath對比用法以下圖示(圖片來源於https://goessner.net/articles/JsonPath/文中):
代碼中如何使用JPath表達式呢?使用JObject.SelectTokens 或 SelectToken方法便可,咱們可使用SelectTokens("jpath")表達式直接快速定位指定的屬性節點,而後就能夠得到該屬性節點的值,若須要該屬性設置值,則能夠經過該節點找到對應的所屬屬性信息進行設置值便可,而動態根據指定位置【通常是某個屬性節點】添加一個新的屬性節點,則能夠直接使用JToken的AddBeforeSelf、AddAfterSelf在指定屬性節點的前面或後面建立同級新屬性節點,是否是很是簡單。原理已說明,最後貼出已封裝好的實現代碼:
using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; namespace Zuowj.EasyUtils { /// <summary> /// JObject擴展類 /// author:zuowenjun /// 2019-6-15 /// </summary> public static class JObjectExtensions { /// <summary> /// 根據Jpath查找JSON指定的屬性節點值 /// </summary> /// <param name="jObj"></param> /// <param name="fieldPath"></param> /// <returns></returns> public static IEnumerable<JToken> FindJsonNodeValues(this JObject jObj, string fieldPath) { var tks = jObj.SelectTokens(fieldPath, true); List<JToken> nodeValues = new List<JToken>(); foreach (var tk in tks) { if (tk is JProperty) { var jProp = tk as JProperty; nodeValues.Add(jProp.Value); } else { nodeValues.Add(tk); } } return nodeValues; } /// <summary> /// 根據Jpath查找JSON指定的屬性節點並賦值 /// </summary> /// <param name="jObj"></param> /// <param name="fieldPath"></param> /// <param name="value"></param> public static void SetJsonNodeValue(this JObject jObj, string fieldPath, JToken value) { var tks = jObj.SelectTokens(fieldPath, true); JArray targetJArray = null; List<int> arrIndexs = new List<int>(); foreach (var tk in tks) { JProperty jProp = null; if (tk is JProperty) { jProp = tk as JProperty; } else if (tk.Parent is JProperty) { jProp = (tk.Parent as JProperty); } else if (tk.Parent is JObject) { jProp = (tk.Parent as JObject).Property(tk.Path.Substring(tk.Path.LastIndexOf('.') + 1)); } if (jProp != null) { jProp.Value = value; } else if (tk.Parent is JArray) //注意不能直接在for循環中對JArray元素賦值,不然會致使報錯 { targetJArray = tk.Parent as JArray; arrIndexs.Add(targetJArray.IndexOf(tk)); } else { throw new Exception("沒法識別的元素"); } } //單獨對找到的數組元素進行從新賦值 if (targetJArray != null && arrIndexs.Count > 0) { foreach (int i in arrIndexs) { targetJArray[i] = value; } } } /// <summary> /// 在指定的JPath的屬性節點位置前或後建立新的屬性節點 /// </summary> /// <param name="jObj"></param> /// <param name="fieldPath"></param> /// <param name="name"></param> /// <param name="value"></param> /// <param name="addBefore"></param> /// <returns></returns> public static void AppendJsonNode(this JObject jObj, string fieldPath, string name, JToken value, bool addBefore = false) { var nodeValues = FindJsonNodeValues(jObj, fieldPath); if (nodeValues == null || !nodeValues.Any()) return; foreach (var node in nodeValues) { var targetNode = node; if (node is JObject) //注意只能對普能單值 的JToken對象(JProptery)容許添加,若不是則應找對應的屬性信息 { targetNode = node.Parent; } var jProp = new JProperty(name, value); if (addBefore) { targetNode.AddBeforeSelf(jProp); } else { targetNode.AddAfterSelf(jProp); } } } } }
用法示例以下代碼:
var jsonObj = new { Root = new { Lv1 = new { col1 = 123, col2 = true, col3 = new { f1 = "aa", f2 = "bb", f3 = "cc", Lv2 = new { flv1 = 1, flv2 = "flv2-2" } } } }, Main = new[] { new{ mf1="x", mf2="y", mf3=123 }, new{ mf1="x2", mf2="y2", mf3=225 } } }; string json = JsonConvert.SerializeObject(jsonObj, Formatting.Indented); Console.WriteLine("JSON1:" + json); var jObj = JObject.FromObject(jsonObj); //JObject.Parse(json); var findResult = jObj.FindJsonNodeValues("Root.Lv1.col3.Lv2.*"); Console.WriteLine("FindJsonNodeValues:" + string.Join(",", findResult)); jObj.SetJsonNodeValue("Main[*].mf2", "*changed value*"); json = JsonConvert.SerializeObject(jObj, Formatting.Indented); Console.WriteLine("JSON2:" + json); jObj.AppendJsonNode("Root.Lv1.col3.Lv2", "LV2-New", JObject.FromObject(new {flv21="a2",flv22=221,flv23=true })); // jObj.AppendJsonNode("Root.Lv1.col3.Lv2", "LV2-2","single value"); json = JsonConvert.SerializeObject(jObj, Formatting.Indented); Console.WriteLine("JSON3:" + json); Console.ReadKey();
控制檯輸出的結果以下:能夠觀察JSON1(原JSON),JSON2(改變了JSON值),JSON3(增長了JSON屬性節點)
JSON1:{ "Root": { "Lv1": { "col1": 123, "col2": true, "col3": { "f1": "aa", "f2": "bb", "f3": "cc", "Lv2": { "flv1": 1, "flv2": "flv2-2" } } } }, "Main": [ { "mf1": "x", "mf2": "y", "mf3": 123 }, { "mf1": "x2", "mf2": "y2", "mf3": 225 } ] } FindJsonNodeValues:1,flv2-2
JSON2:{ "Root": { "Lv1": { "col1": 123, "col2": true, "col3": { "f1": "aa", "f2": "bb", "f3": "cc", "Lv2": { "flv1": 1, "flv2": "flv2-2" } } } }, "Main": [ { "mf1": "x", "mf2": "*changed value*", "mf3": 123 }, { "mf1": "x2", "mf2": "*changed value*", "mf3": 225 } ] }
JSON3:{ "Root": { "Lv1": { "col1": 123, "col2": true, "col3": { "f1": "aa", "f2": "bb", "f3": "cc", "Lv2": { "flv1": 1, "flv2": "flv2-2" }, "LV2-New": { "flv21": "a2", "flv22": 221, "flv23": true } } } }, "Main": [ { "mf1": "x", "mf2": "*changed value*", "mf3": 123 }, { "mf1": "x2", "mf2": "*changed value*", "mf3": 225 } ] }
好了,本文的內容就分享到這裏。更多以往的編碼實用技巧詳見:《近期開發項目中用到的編碼小技巧彙總說明(二)》;
舒適提示:近期還會分期更多編程實用技能,歡迎關注評論,謝謝!