想想某一天,你在看LOL攻略的时候,系统突然崩溃了,接着浏览器出现了密密麻麻的LOL帐号和密码,你一定在想:“天啊,这次要发财了,说不定里面有超凡号或者王者号,我得赶紧全部记下来。”然而说完你就惊呆了,那么多的帐号密码,而且全部写在了Json里面,一个一个复制粘贴要记到什么时候啊。。。如果这时候我在你身边,我一定会帮助你的,前提是,要分几个王者号给我噢。。。
言归正传。
上面举的例子虽然有点不太现实,但其实是想和大家说明一个问题,如果要解析Json或XML,请不要使用检索字符串的方式!!!
那么在说明如何解析Json和XML之前,我们先来搞清楚两个概念:序列化和反序列化。
「序列化,即Serialization,是一个将对象的状态信息转变为可以存储或传输的形式的过程。」
「反序列化,即Deserialization,顾名思义是一个将可以存储或传输的序列转变为某个对象的状态信息的过程。」
从上面的定义,我们可以得出两个结论:
① 这两个过程互为逆过程;
② 无论是序列化还是反序列化,对象的状态信息都与一段序列相对应。
下面我们就来看看如何用C#语言进行Json和XML的序列化和反序列化。
Json的序列化与反序列化
① 使用Newtonsoft.Json库
首先我们先来看一段代码:
1 public void JsonDeserialize(string jsonString) 2 { 3 //Json的反序列化操作 4 JObject jObject = JsonConvert.DeserializeObject(jsonString) as JObject; 5 //清空listBox的结果 6 listBoxResult.Items.Clear(); 7 //将解析得到的数据显示到listBox中 8 listBoxResult.Items.Add("Name : " + jObject["name"] ?? "null"); 9 listBoxResult.Items.Add("Doing : " + jObject["doing"] ?? "null"); 10 listBoxResult.Items.Add("HandleContent : " + jObject["handleContent"] ?? "null"); 11 listBoxResult.Items.Add("DateTime : " + jObject["dateTime"] ?? "null"); 12 }
上面这个方法的作用是,接收一段Json序列作为参数,在完成反序列化后,将所有字段的值输出到ListBox中,下面是简单的测试:
可以看到,仅仅用了一行代码,Json的内容就按照字段全都被获取到了,下面就来好好了结一下Newtonsoft.Json这个库的用法吧。
Newtonsoft.Json是.NET平台中一个非常流行且高性能的Json序列化和反序列化开源库,提供了许多用于序列化和反序列化Json的类及方法,除此之外它还提供了用Linq操作Json、将Json转换成XML的功能,相较于传统的用于序列化和反序列化的DataContractJsonSerializer类更好用,更高效。本文只介绍其中比较重要的JsonConvert类和几个常用的类型,更多内容大家可以访问Json.NET的主页进行了解。
在介绍使用方法前,先来说说Newtonsoft.Json.dll的引入。从VS2013开始,ASP.NET的项目均自带了Newtonsoft.Json.dll,直接使用即可,而其他版本或者其他项目如果没有的话,可以进入其官方主页,找到Github托管的地址进入并下载。下面演示引用该动态链接库的流程,
① 点击菜单栏的项目->添加引用,或者在解决方案资源管理器中找到项目,右键引用属性,点击添加引用,然后就会打开该窗口。
该窗口打开后,如果是第一次添加Newtonsoft.Json.dll,请找到右下方的浏览按钮,并选择Newtonsoft.Json.dll;若以前添加过该库,可以点击左侧的浏览菜单,并点击最近,右侧会显示以前添加过的库的信息,勾上需要的库,最后点击右下方的确定即可。
添加了动态链接库还没完,要想使用其中的类和方法,需要引入相应的命名空间(如果你不想每定义一个对象就写一长串命名空间的话),本文主要用到的命名空间是Newtonsoft.Json以及Newtonsoft.Json.Linq。
完成这两步之后就可以舒舒服服的开始解析我们伟大的Json了。
Now,先来介绍本库比较重要的类,JsonConvert。JsonConvert是个非常实用的工具类,它提供了用于反序列化的DeserializeObject方法和DeserializeObject<T>方法,以及用于序列化的SerializeObject方法。由于这些方法都是静态方法,因此无需创建JsonConvert对象,直接使用妥妥的(事实上JsonConvert是个静态类,根本无法被实例化)。
在最开始演示的例子中,我们使用了JsonConvert.DeserializeObject方法,这是最原始的Json反序列化方法,它接收string类型的参数并返回object类型的对象。那么你也许会问到,它返回的对象究竟是什么类型???想要真正获得该对象的信息,就必须将其强制转换成具体的派生类。那么,我们可以使用GetType方法来一探究竟。
1 private void btnDeserialize_Click(object sender, EventArgs e) 2 { 3 //JsonDeserialize(textBox1.Text); 4 var secretObject = JsonConvert.DeserializeObject(textBox1.Text); 5 MessageBox.Show(secretObject.GetType().ToString()); 6 }
我们先在一个按钮的点击事件对应的方法中解析输入的Json序列,由于我们还不确定JsonConvert.DeserializeObject方法返回什么对象,因此我们先用隐式类型对象去引用该方法返回的结果,然后调用其GetType方法,获取该对象所属类型的元数据,最后将对象的完整类型名显示在对话框当中。执行这段代码,你将会看到如下结果:
也许这时候在你的心里又有一万个草泥马呼啸而过,“What?还没搞清楚JsonConvert类的用法,怎么又跑出了一个奇怪的类?”
但是没关系,接下来我会对大家的谜团进行逐一的解释。
在Newtonsoft.Json.Linq命名空间中,定义了一些用于表示不同形式的Json字符串的类,它们包括JObject、JProperty、JToken、JArray、JValue、JContainer等等。
那么在这些类当中,JToken充当着标杆的角色,它是一个抽象类,并实现了IJEnumerable<T>接口,而IJEnumerable<T>接口继承自IEnumerable<T>接口,这意味着所有继承JToken的类都可以使用foreach语句进行遍历,而接下来要介绍的这些类,全都派生自JToken。另外,JToken还定义了First、Last、Parent、Root、Item、Previous、Next等遍历经常会用到的属性,因此在使用这些类的时候,遍历、查找具体类等操作就显得非常的简单。说了这么多,是时候说说JObject了,这是个JToken的派生类,同时也是实现了JContainer接口、ICollection<T>、IKeyValuePair<T>,这意味着JObject可以存储多个继承JToken类型的对象,同时也可以遍历它们,还可以用键去获取相应的值,也许你还不清楚这些有什么用,那么我们先来了解一下Json的基本结构:
一般来说,被大括号括起来的内容可以看成是一个整体的(一个对象的),而被中括号括起来的内容,可以看成是一组内容(一个数组),在一个复杂的Json结构中,大括号里面的某个键的值可以在嵌一个大括号,代表该属性值为另一个Json对象,同理一个大括号中的某个键的值可以嵌一个中括号,代表该属性值是一个数组,如
1 //对于这种Json来说,可以看成是一个对象,里面包含了name、doing、handleContent和dateTime属性。 2 { 3 "name":".NET Coder", 4 "doing":"Deserialization", 5 "handleContent":"Json", 6 "dateTime":"2017-09-11 12:12:12" 7 } 8 9 //对于这种Json来说,可以看成是一个数组对象,里面包含了两个对象。 10 [ 11 { 12 "name":".NET Coder", 13 "doing":"Deserialization", 14 "handleContent":"Json", 15 "dateTime":"2017-09-11 12:12:12" 16 }, 17 { 18 "name":"Java Coder", 19 "doing":"Deserialization", 20 "handleContent":"Json", 21 "dateTime":"2017-09-22 21:21:21" 22 } 23 ]
在上一个演示的例子中,我们用GetType方法去判断完成反序列化之后,实际返回的类型是什么;那么接下来我们就将上面这两种不同结构的Json序列放入程序中解析,看看得到的对象是不是都是JObject。
成功得到了JObject类型的对象。
这次我们竟然得到了JArray?!
这下我们也许就明白了,JObject用于表示一个完整的Json对象结构,而JArray用于表示一个Json数组,其中会包括一个或多个Json对象。
在获得JObject对象之后,我们可以用属性名(键)去获得相应的属性值,也可以使用Next、Previous等属性去逐个访问JObject的属性。
而获得JArray对象之后,我们可以使用for或者foreach语句去遍历该对象内的元素,然后根据某个属性名或Next、Previous等属性去访问当前元素某个属性的属性值。
那么JProperty和JValue类型又有什么作用呢?
我们可以来调试一下程序,并观察JObject对象内的结构,
如图我们在使用foreach循环遍历JObject内部的属性,可以看到,First属性指向了JObject的第一个属性,而它的类型是JProperty,由此我们可以知道,JProperty类用于描述一个Json对象中的某一个属性,在其内部包含了一个KeyValuePair<string, JToken>的成员变量,用于存储该Json属性的键值对,而这个值,其实就是JValue类型的变量。
那么到这里为止,我们已经掌握了DeserializeObject方法的用法了,但是很明显,这种方法解析Json数据实在太麻烦了,我们需要把所有Json对象的属性一个个获取到,然后才能得到其中的值,有没有更简介的办法呢?
有!有!有!重要的事情说三遍。
下面该轮到DeserializeObject<T>方法出场了,这是个泛型方法,指定T的类型并传入Json字符串作为参数后,该方法会完成反序列化并返回一个T类型的对象。那么我们该指定什么类型给该方法呢?这个类型需要根据Json数据的层次关系自行设计,如果大家是第一次使用,不知道该如何设计,那么我们来让VS帮我们设计,下面给大家介绍一个神奇的功能,VS的自动建类功能:
还是这段Json序列,假设我们需要将其反序列化,现在却不知道怎么设计类,那么我们可以先复制这段Json序列,
1 { 2 "name":".NET Coder", 3 "doing":"Serialization", 4 "handleContent":"Json", 5 "dateTime":"2017-09-11 12:12:12" 6 }
打开VS,在菜单栏找到编辑->选择性粘贴,点击将Json粘贴为类,就会出现下面的结果,
原本空白的地方,突然出现了一个类的定义(如果Json有多个层级,VS会帮我们生成多个类),这个类清晰的反映了这段Json的结构,此时我们只需要将这个自动生成的类,用到方法里就好了:
我们改写了JsonDeserialize方法,并使用了由VS生成的类,可以看到我们不需要再根据属性名去获得属性值了,也不用再担心忘记有什么属性或者属性名写错,只需要在对象后面加上一点,该对象的所有属性都会出现在选择列表中。是不是突然觉得眼前一亮,一身轻松了呢?但其实这个做法隐藏了一点点小问题,因为VS是根据Json原来的属性名来定义属性的,这就要求Json的属性名遵循C#的标识符定义规则,然后事实上,在Json中可能会出现不符合C#标识符定义规范的属性名,这并没有错,如下面这段Json:
1 { 2 "name":"money", 3 "price":"66.6", 4 "int":"64", 5 "a-b":"c-d", 6 "@#$":"sdfsdf" 7 }
可以看到,虽然VS会用_去代替不合法的字符串并按照规范生成属性名,但是这样子的属性名已经失去了它的意义,我们无法得知这个属性表达了什么含义,为了解决这个问题,我们可以使用C#的注解来给该类的属性制定数据协定。
DataContract和DataMember注解来自于命名空间System.Runtime.Serialization,这个命名空间所在的动态链接库项目默认是没有引入的,需要手动引入,我们只需要按照上面引入Newtonsoft.Json.dll的方式引入就好,但是需要注意的是,System.Runtime.Serialization所在的动态链接库在VS程序集中是提供了的,我们只需要在添加引用界面,点击程序集,并在右侧的列表找到System.Runtime.Serialization并勾选,然后点击右下角的确定按钮即可。
完成这些步骤之后我们就可以改造Json的实体类,为其添加数据协定。什么是数据协定呢,即数据的发送方和接收方不需要使用相同的类型,只需要遵循数据协定即可。为了使该类能够添加数据协定,需要在类名上方添加[DataContract]注解,说明该类的属性使用了数据协定,并且允许将该类进行序列化,如果不加这个注解,就不能使用[DataMember]注解了。添加[DataContract]注解后,我们就可以在对应的属性上添加[DataMember]注解,该注解有4个参数:
参数名 | 作用 |
Name | 标识该属性对应的Json属性名 |
Order | 标识该属性序列化和反序列化的顺序 |
IsRequired | 标识该属性在读取或反序列化时是否必须存在,类型为bool |
EmitDefaultValue | 标识在序列化的过程中是否使用该属性的默认值来序列化 |
上图我们使用了Name参数来设置每个属性对应的Json属性名,从而增强了该类在程序中的可读性,同时又不会对序列化和反序列化的过程产生麻烦。下面再改写一下JsonDeserialize方法吧:
测试成功!!!
另外再补充一个小知识,当我们需要反序列化很多简单的Json时,为了避免创建过多的实体类,我们可以使用dynamic动态类型来代替自定义的实体类,在这里我们改写上述例子,使用dynamic类型来实现相应功能:
在这个例子中,我没有定义新的类,而是使用了动态类型,这种类型的对象有一个特点:运行时检查。在使用这种对象的时候我们要注意,无论是使用它的属性,还是调用它的方法,编译器都不会对其进行编译时检查,也就是说我们必须保证属性名或者方法名没有写错,否则会出现表达式为空或者引发异常的现象。
调用不存在的属性返回Null。
调用不存在的方法引发异常。
在没有错误的情况下,成功运行!
到这里反序列化的介绍算是告一段落了,接下来我们讲讲使用JsonConvert.SerializeObject方法完成序列化操作。
前面那么多操作的目的是为了解析Json字符串,让我们能方便的获得其中的信息,但是如果我们不是信息的接收方而是发送方呢,我们要怎么构造Json字符串?显然不可能用字符串拼接的方法,效率低下,难以排错。因此今天我们就要用一行代码来完成这件事情。
没错,就只有一行代码!!!
看吧,是不是一行代码就让一个活生生的对象瞬间变成了一串Json了呢!!!
② 使用DataContractJsonSerializer类
其实本人并不想讲解这个类,第一使用过程非常麻烦,第二效率低下,实际工作中也不会用它,但是从学习的角度我们还是要了解一下这个类的存在,并且这个类序列化和反序列化Json的过程和XML的序列化和反序列化很相似,因此还是有必要让大家了解一下。
在此不多说其他废话,直接上代码:
DataContractJsonSerializer序列化
1 public void OldJsonSerialize() 2 { 3 DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HandleObject)); 4 //创建实体类 5 HandleObject jObject = new HandleObject() { 6 Name = ".NET Coder", 7 Doing = "Serialization", 8 HandleContent = "Json", 9 DateTime = DateTime.Now.ToString("yyyy-MM-ss") 10 }; 11 //创建内存流,用于存储序列化后得到的内容 12 MemoryStream ms = new MemoryStream(); 13 //将对象序列化后存入内存流 14 serializer.WriteObject(ms, jObject); 15 //将内存流的内容转换成字符串并输出到文本框 16 textBox1.Text = Encoding.UTF8.GetString(ms.ToArray()); 17 }
DataContractJsonSerializer反序列化
1 public void OldJsonDeserialize() 2 { 3 DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(HandleObject)); 4 //将Json字符串转换成字节数组 5 byte[] content = Encoding.UTF8.GetBytes(textBox1.Text); 6 //用字节数组初始化一个内存流对象 7 MemoryStream ms = new MemoryStream(content); 8 //完成反序列化操作 9 HandleObject handleObject = serializer.ReadObject(ms) as HandleObject; 10 //将解析得到的数据显示到listBox中 11 listBoxResult.Items.Add("Name : " + handleObject.Name ?? "null"); 12 listBoxResult.Items.Add("Doing : " + handleObject.Doing ?? "null"); 13 listBoxResult.Items.Add("HandleContent : " + handleObject.HandleContent ?? "null"); 14 listBoxResult.Items.Add("DateTime : " + (handleObject.DateTime.ToString() ?? "null")); 15 }
DataContractJsonSerializer类和JsonConvert类最大的不同就在于,DataContractJsonSerializer类将操作流的细节暴露了出来,需要由用户自行完成,相比JsonConvert直接使用字符串要麻烦不少。