一、背景
JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
Json广泛的用于数据传输的时候(比如Web的前后端数据传输),对于这种场景我们不会刻意的去控制对象序列化后,Json字符串中的字段顺序。因为在这种场景下,我们的要求只是能用就好。但一些特殊的情况,例如需要对一个对象进行Json序列化,并对产生的Json字符串进行RSA签名,那就一定要控制Json字符串中的字段顺序了。因为如果不严格的控制字段顺序,明明是一样的对象会因为序列化后Json字符串中的字段顺序不一致,导致产生的签名结果不一致。
那么,在C#和Python里,有什么方法可以控制对象字段在Json序列化时的顺序呢?
注:
后面的实现中,为了保证展示的效果,将使用以下测试数据:
用户(User)字段:
字段名 | 字段类型(C#/Python) | 值 |
Uid | int/int | 100000 |
Username | string/str | hiiragi |
Password | string/str | 0123456789abcdef0123456789abcdef |
Phone | string/str | 13412341234 |
Enable | bool/bool | true |
要求输出的Json字符串中,所有的字段以字段名升序排序。
二、Python 下的实现
这次我们先讲一下Python下的实现,因为比较简单。
Python下,我们常常会把需要序列化的数据存到一个字典(dict)对象中,但在Python中,字典对象是利用散列表实现的,这就导致了默认的字典对象是无序的。也就是说,您存入字典的键值对顺序可能会因为Python自动扩容字典时的重新哈希而被破坏。
例如下面的代码:
1 2 3 |
d = {"key": "value"} for i in range(i): d[f"key{i}"] = f"value{i}" |
上面的代码笔者在运行完后输出字典对象d,解释器的输出结果是这样的:
很明显,这个顺序不是我当时存入时使用的顺序。
为了解决这个问题,Python提供了一个叫做OrderedDict的字典变种,它能够在添加键时保持顺序,所以在每一次对键进行迭代的时候都能保证顺序是一定的。
现在,我们就以第一章中的测试数据为例,进行一个简单的演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from json import dumps from collections import OrderedDict user = { "Uid": 100000, "Username": "hiiragi", "Password": "0123456789abcdef0123456789abcdef", "Phone": "13412341234", "Enable": True } # 对现有的 user 字典的所有键进行排序,并根据排序后的顺序重新构建 OrderedDict user = OrderedDict([ (key, user[key]) for key in sorted(user) ]) json = dumps(user) print(json) |
运行以上的代码,可以获得最后的结果:
1 |
{"Enable": true, "Password": "0123456789abcdef0123456789abcdef", "Phone": "13412341234", "Uid": 100000, "Username": "hiiragi"} |
可以,正是符合我们想要的结果。由此可以得出结论:在Python中,如果需要控制Json序列化后Json字符串中的字段顺序,可以使用Python提供的OrderedDict,并将想要的字段顺序依次放入字典中后,再序列化即可。
顺带一提,如果在Python中,需要对Json反序列化后产生的字典也能根据Json字符串中字段的顺序排列,可以通过设置json.loads函数的“object_hook”参数为OrderedDict即可,这样反序列化的结果便是一个OrderedDict,且其中的字段顺序与Json字符串中的字段顺序一致:
1 2 3 4 5 6 |
from json import loads from collections import OrderedDict json = '{"Enable": "true", "Password": "0123456789abcdef0123456789abcdef", "Phone": "13412341234", "Uid": 100000, "Username": "hiiragi"}' user = loads(json, object_hook=OrderedDict) |
三、C# 下的实现
相比于Python,要控制Json序列化后产生的Json字符串中字段的顺序,C#就比较麻烦了。
一个是因为C#没有自带的Json序列化库(emmmmm,咱们就不要提System.Web.Script.Serialization. JavaScriptSerializer了好么,它不仅要手动添加框架引用System.Web.Extenisions,而且存在感极低,连亲爹微软都不怎么用它),二是C#里一般不用字典去存待序列化的数据,而是新建一个模型类的。
在.net平台下,现在最常用的Json序列化包一定就是Newtonsoft.Json里,所以接下来我们就以Newtonsoft.Json为例进行讲解:
先创建一个User模型类:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class User { public int Uid { set; get; } public string Username { set; get; } public string Password { set; get; } public string Phone { set; get; } public bool Enable { set; get; } } |
我们先看看不做任何处理时,利用Newtonsoft.Json序列化后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using Newtonsoft.Json; static void Main() { User user = new User() { Uid = 100000, Username = "hiiragi", Password = "0123456789abcdef0123456789abcdef", Phone = "13412341234", Enable = true }; string json = JsonConvert.SerializeObject(user); Console.WriteLine(json); } |
输出结果:
1 |
{"Uid":100000,"Username":"hiiragi","Password":"0123456789abcdef0123456789abcdef","Phone":"13412341234","Enable":true} |
C#要控制Newtonsoft.Json对指定类型序列化时的字段顺序,有以下两种方法:
1.利用 JsonProperty 属性
先讲一个非常简单但拓展性不是特别好的方法吧,那就是JsonProperty属性:
首先引入命名空间Newtonsoft.Json:
1 |
using Newtonsoft.Json; |
好了,现在就可以在模型类中的字段上添加属性“JsonProperty”了。我们可以按F12查看从元数据,我们可以看到JsonPropertyAttribute类有一个公开属性“Order”,这个属性的摘要是:
Gets or sets the order of serialization of a member.
获取或设置一个成员的序列化顺序。
所以我们为各个属性添加JsonProperty属性,并根据情况设置其Order属性的值,比如本文之前提到的情景,可以修改User模型类为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class User { [JsonProperty(Order = 4)] public int Uid { set; get; } [JsonProperty(Order = 5)] public string Username { set; get; } [JsonProperty(Order = 2)] public string Password { set; get; } [JsonProperty(Order = 3)] public string Phone { set; get; } [JsonProperty(Order = 1)] public bool Enable { set; get; } } |
完成后重新运行序列化代码,输出序列化后的Json字符串:
1 |
{"Enable":true,"Password":"0123456789abcdef0123456789abcdef","Phone":"13412341234","Uid":100000,"Username":"hiiragi"} |
完美达到效果。
2.自定义 ContractResolver
刚刚提到的利用JsonProperty属性来手动调整Newtonsoft.Json对一个模型类中字段序列化顺序的方法,有一个非常明显的不足,那就是后期对这个模型类拓展时,每增加一个新的字段都需要重新计算各个字段的Order值,拓展性非常的差。
那还有没有什么方法呢?答案当然是有的,那就是我们自定义一个ContractResolver,自定义的ContractResolver可以在序列化时被Newtonsoft.Json所调用,并通过CreateProperties方法的输出来进一步的控制Newtonsoft.Json序列化对象时字段输出的顺序。
不过从零开始重写一个ContractResolver实在还是有点麻烦,所以我们可以偷把懒,以Newtonsoft.Json提供的DefaultContractResolver为基类,重写它的CreateProperties方法~:
引入三个命名空间:System.Linq(用于排序)、Newtonsoft.Json(CreateProperties方法中的第二个参数类型所在的命名空间)和Newtonsoft.Json.Serialization(DefaultContractResolver类所在的命名空间)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; public class OrderedContractResolver : DefaultContractResolver { protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { return base.CreateProperties(type, memberSerialization).OrderBy(u => u.PropertyName).ToList(); } } |
重写的CreateProperties方法中的代码被缩写成一句话了,如果要展开来,可以展开成下面两行:
1 2 |
IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization); return properties.OrderBy(u => u.PropertyName).ToList(); |
第一行负责调用父类(也就是DefaultContractResolver类)的CreateProperties方法,并将取得返回值;然后再利用Linq的拓展方法,根据列表中各元素(JsonProperty对象)的属性名称进行排序,最后重新整理为列表并返回。
完成OrderedContractResolver的编写后,并不能直接看到效果,因为这个OrderedContractResolver需要在序列化对象时作为参数传给SerializeObject方法,所以我们序列化对象的代码就变成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
User user = new User() { Uid = 100000, Username = "hiiragi", Password = "0123456789abcdef0123456789abcdef", Phone = "13412341234", Enable = true }; string json = JsonConvert.SerializeObject(user, new JsonSerializerSettings() { ContractResolver = new OrderedContractResolver() }); Console.WriteLine(json); |
运行查看效果:
1 |
{"Enable":true,"Password":"0123456789abcdef0123456789abcdef","Phone":"13412341234","Uid":100000,"Username":"hiiragi"} |
完美达成效果。
四、小结
本篇文章主要讨论了在C#和Python中,如果控制对象在Json序列化时各个字段的输出顺序。
Python相对简单,只需要使用OrderDict按顺序填入字段名和字段值即可。
C#则以Newtonsoft.Json为例,可以使用JsonProperty属性和自定义ContractResolver类来控制Json序列化时字段的顺序。通过设置JsonProperty属性来调整Json字段顺序的方式虽然简单,但拓展性并不高,适合于属性相对较少的模型;而自定义ContractResolver类的方式可以灵活的控制对象在序列化时各个字段的输出,例如调整顺序,删除字段等,但相对于JsonProperty属性的方式而言,比较复杂度。
五、参考文档:
1.《Order of serialized fields using JSON.NET》
小柊
2018年8月13日 22:02:52