看文章標題就知道,本文的主題就是關於JSON,JSON轉換器(JsonConverter)具有將C#定義的類源代碼直接轉換成對應的JSON字符串,以及將JSON字符串轉換成對應的C#定義的類源代碼,而JSON操作技巧則說明如何通過JPath來快速的定位JSON的屬性節點從而達到靈活讀寫JSON目的。
一、JSON轉換器(JsonConverter)使用及原理介紹篇
現在都流行微服務,前后端分離,而微服務之間、前后端之間數據交互更多的是基於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工具的界面吧,如下圖示:

工具界面很簡單,下面簡要說明一下操作方法:
class類源代碼轉換成Json字符串:先將項目中定義的class類源代碼復制粘貼到Class Code文本框區域【注意:若有繼承或屬性本身又是另一個類,則相關的class類定義源代碼均應一同復制,using合並,namespace允許多個,目的是確保可以動態編譯通過】,然后點擊上方的【Parse】按鈕,以便執行動態編譯並解析出Class Code文本框區域中所包含的class Type,最后選擇需要生成JSON的class Type,點擊中間的【To Json】按鈕,即可將選擇的class Type 序列化生成JSON字符串並展示在右邊的Json String文本框中;
示例效果如下圖示:(支持繼承,復雜屬性)

有了這個功能以后,API寫好后,只需要把ACTION方法的入參class源代碼復制過來然后進行class to JSON轉換即可快速生成入參JSON,不論是自己測試還是寫文檔都很方便。建議使用markdown語法來編寫API文檔。
Json字符串轉換成class類定義源代碼:先將正確的JSON字符串復制粘貼到Json String文本框中,然后直接點擊中間的【To Class】按鈕,彈出輸入要生成的class名對話框,輸入后點擊確定就執行轉換邏輯,最終將轉換成功的class定義源代碼展示在左邊的Class Code文本框區域中;
示例效果如下圖示:(支持復雜屬性,能夠遞歸生成JSON所需的子類,類似如下的Address,注意暫不支持數組嵌套數組這種非常規的格式,即:[ [1,2,3],[4,5,6] ])

JsonConverter工具實現原理及代碼說明:
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數據的讀寫操作技巧。
一般操作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
}
]
}
好了,本文的內容就分享到這里。更多以往的編碼實用技巧詳見:《近期開發項目中用到的編碼小技巧匯總說明(二)》;
溫馨提示:近期還會分期更多編程實用技能,歡迎關注評論,謝謝!
