跳到主要内容

JSON 键排序怎么做:按字母递归排好,让数据稳定可比

讲清 JSON 键排序的两种做法,按字母排和递归进嵌套对象,以及它在 diff 友好、版本控制、可读性、规范化上的真实用途,附一段乱序到排序的完整例子。

发布于 作者 李雷
#json #键排序 #版本控制 #diff

JSON 键排序怎么做:按字母递归排好,让数据稳定可比

同一份数据,两个程序写出来键顺序不一样,这是 JSON 最常见的麻烦。对象在规范里本就是无序的,序列化时键按什么顺序落地,取决于写入它的那段代码,而不是数据本身。于是配置文件提交进 git 时,diff 里满屏移位的行,可你心里清楚什么都没改。把键排成一个固定顺序,这些问题大半就消失了。

键排序到底排的是什么

排序只动对象键的先后,不动值。{"port":80,"host":"x"} 排成 {"host":"x","port":80},80 还是 80,字符串照抄,布尔和 null 直接透传,数组里的元素一个不少。这是关键的一点:它是把同一份数据换个键顺序重新写一遍,语义完全不变,只是表现形式被规整了。

按字母排是最常用的规则。比较的是键名字符串本身,a 在 b 前,b 在 c 前。需要倒序时切到降序即可。还有一个细节是大小写:不区分时,"Name"、"name"、"NAME" 当成同样的字母归在一起,读起来顺;区分时按码位排,大写整段排在小写前。选哪个,看你下游工具怎么比较字符串。

递归进嵌套对象

光排最外层往往不够。真实配置常常嵌套三四层,只理顶层等于没理。递归排序会走遍每一层:对象里的对象排,数组里的对象也排。比如 {"server":{"port":80,"host":"x"},"app":{"name":"y"}},递归排完是 app 在 server 前,而 server 内部 host 在 port 前,从上到下一致。

但递归不是总要开。如果某个嵌套小节是你特意手调过顺序的,比如一段按执行步骤排列的字段,递归会把它也重排,破坏你的本意。这时关掉递归,就只动最外层键,里面的块原样保留。

一段乱序 JSON 排序后

拿一段顺手堆出来的配置看效果。输入:

{"timeout":30,"app":{"port":80,"host":"api.local"},"retries":3}

开递归、按字母升序、不区分大小写,输出:

{
  "app": {
    "host": "api.local",
    "port": 80
  },
  "retries": 3,
  "timeout": 30
}

顶层 app、retries、timeout 按字母落位,app 内部 host 排到了 port 前。值一个没变,只是顺序稳定了。换台机器、换个人、换个写入库,只要排序规则一样,输出逐字节相同。

它真正有用的几个场景

第一是 diff 友好和版本控制。两份配置排成同一个键顺序后,git diff 只在某个值真的变了时才亮起来,审查的人不再划过一堆噪音。第二是快照测试稳定:后端返回不保证键顺序,排好键再存快照,测试就只在数据真不同时才失败,不会两次运行间一会儿绿一会儿红。

第三是可读性。庞大的配置靠堆积长大,新键谁顺手就加在哪。排一次,找 "timeout" 或 "retries" 就在可预期的位置,不用 grep 整个文件。第四是规范化:把 JSON 哈希后当缓存键时,语义相同但键顺序不同的对象会算出不同哈希,先排键得到规范形态再哈希,相同数据就总映射到同一个键。

数组要不要排

默认不排数组,因为位置常常有含义,清单里的步骤、表格里的行,把 [3,1,2] 排成 [1,2,3] 会改变数据要说的话。确实想整理时,可以只给基本类型数组(全是数字、全是字符串、全是布尔)排序,而含对象或嵌套数组的数组原样保留,结构不被动。这条要按数据本身来判断,不能一刀切。

我自己最常用它的时机,是把两个微服务各自吐出来的配置对齐着提交。以前每次部署 diff 都是几十行移位,review 的同事得逐行确认没有真改动,很费神。统一过一遍键排序后,diff 平均只剩两三行,有意义的那一处一眼就看到,这种确定性带来的安心比省下的时间更值。

想直接试,用 JSON 键排序 粘进去选好规则就能看到结果。如果你的目标是比较两份 JSON 而不是排序,JSON 对比 更直接,先各自排键再对比,差异会干净很多。


Made by Toolora · Updated 2026-06-13