看文章标题就知道,本文的主题就是关于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 } ] }
好了,本文的内容就分享到这里。更多以往的编码实用技巧详见:《近期开发项目中用到的编码小技巧汇总说明(二)》;
舒适提示:近期还会分期更多编程实用技能,欢迎关注评论,谢谢!