前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JSON 这么可爱,让我们用千字短文吃透它吧!

JSON 这么可爱,让我们用千字短文吃透它吧!

原创
作者头像
amc
修改2022-10-20 11:46:20
1.9K3
修改2022-10-20 11:46:20
举报
文章被收录于专栏:后台全栈之路后台全栈之路

JSON,一个伟大的协议,前端工程师的卓越发明!相信 99% 的程序员都认识 JSON,它作为前后端交互的热门协议,因其易理解、简单、灵活和超强的可读性,得到了互联网的广泛欢迎,甚至很多微服务之间的传输协议中也得到应用。

但是笔者在开发一个 Go 的 JSON 编解码库的过程中,除了自己趟过各种奇奇怪怪的问题之外,也认识到广大程序员们对 JSON 各种奇奇怪怪的用法和使用姿势。在处理解决这些问题之后,笔者萌生了对 JSON 进行进一步科普和介绍的想法。

相信我,看完这篇文章,你就可以吃透这个可爱的 JSON 了。

这不是万字长文,所以答应我,不要 too long, don't read 好吗?


JSON 是什么

这个问题似乎很容易回答:JavaScript Object Notation,直译就是 JavaScript 对象表示。

然而,这个命名中的 “JavaScript” 是个很大的误导,让人以为 JSON 是附属于 JavaScript 的。其实不然,JSON 是完全独立于任何语言之上的一个对象表示协议,甚至从我个人的角度来说,它非常的不 “JS”。

关于 JSON 的 “常识”

从大家的认知中,相信以下的几点是常识:

  • JSON 可以是对象(object),使用 {...} 格式包起来
  • JSON 可以是是数组(array),使用 [...] 格式包起来
  • JSON 内的值可以是 string, boolean, number,也可以进一步嵌套 object 和 array
  • JSON 也有特殊字符需要转义,最显而易见的就是双引号 "、反斜杠 \、换行符 \n\r
  • JSON object 的键(key)必须是 string 格式
  • JSON 可以通过 object 和 array 类型实现无限层级的嵌套

好了,懂了上面几点,其实也就弄懂了 JSON 90% 甚至是 99% 的应用场景了。程序员们也足以可以实现简单的 JSON 编码逻辑。

如果你想知道剩下那些让人掉大牙的 1%,欢迎你往下看;如果你想要自己开发一个 JSON 编解码库,以下内容也能够让你少走很多弯路:


JSON 标准规定了什么

在了解各种 JSON 的坑之前,我们先来了解一下 JSON 标准本身。

现行通行的 JSON 标准是 ECMA-404,这篇协议总共有14页,但除去封面、封底、目录、简介、版权声明,正文只有5页,并且其中3页大部分是图片。所以笔者推荐所有的程序员都把这篇文档通读一遍,恐怕这是大部分人唯一能完整读完的主流协议了(狗头)。

所以啊,“可爱” 的 JSON 可真不是标题党——试想这么短小的协议,怎不可谓可爱呢!

通读了文档之后我们可以发现,除了前文提及的几个常识之外,下面有几个知识点估计大家很少留意:

  • JSON 是用来承载 unicode 字符的,这一点在标准中明确提及
  • JSON 标准中其实并没有 boolean 这个类型,但是 truefalse 被并列为单独的两个类型
  • 作为最外层的 JSON 类型,并不限定为 object 或 array,实际上 string, boolean, number, 甚至 null 也是可以的
  • JSON 数字表示可以使用科学计数法,可能许多人在实际应用中没留意过
  • JSON 明确说明不支持 +/-Inf 和 NaN 这两组在 IEEE 754 中规定的特殊数值

我们解读一下上面这些知识点带来的影响:

  • unicode 字符:
    • JSON 传输的应当是可视化字符,而不应该也无法承载不可读的二进制数据
    • 换句话说,请尽量不要用 JSON 来传输二进制数据
  • 没有 boolean 类型
    • 这个问题不大,主要是对各种库的使用上——有些库提供了 boolean 类型分类,而有些库则按照标准协议分为 true 和 false 两种类型,请注意区分
  • 外层类型不限定
    • 其实这影响不大,但是这使得 JSON 多了一个额外功能: 当我们要把包含换行符的文本压在一行内,但又要保持高可读性的时候,我们可以将文本序列化为 JSON
    • 这个特性在打日志的时候特别有用
  • 科学计数法:
    • 这主要是在解析 JSON 数据时,需要注意兼容
  • 特殊浮点值:
    • 这个问题可大可小,大部分情况下不会遇到,但是一旦出现了,会导致整个序列化过程失败。开发者们需要谨慎处理浮点数,下文会进一步提及

JSON 没有规定什么?

  • JSON 并没有严格限定文本的编码格式
  • JSON 数字是十进制的,没有限制绝对值大小,也没有限制小数点后的位数
  • JSON 没有明确规定 ASCII 的控制字符和不可见字符的传输格式
  • JSON 没有限制 object 的 key 所使用的字符
  • JSON 没有明确说明 object 的 key 之间是有序的还是无序的

为什么列出这几点?让我们继续往下看:


JSON 的常见 “坑位”

JSON 如此简单,但也正因为它的特性,我们会不知不觉地落入一些圈套中。下面我列几个常见的坑,读者看看能不能对号入座:

没搞明白编码格式导致解码出错

前面说到,JSON 明确声明自己是用于承载 unicode 的。但是,unicode 除了规定每个字符码的含义(码点)之外,还包含另外一个重要规范,那就是如何将这些字符串成字符流,这就是我们常说的 UTF-8、UTF-16BE、

UTF-16LE 等等概念。而 JSON 并没有对此作明确限定。这就导致了在 JSON 的编码与解码端,如果没有约定好,那么就会出现乱码。

笔者曾经与一个合作伙伴的开发工程师对接过 JSON,对方使用 Java 解码我发出的原始数据时出现乱码。我告诉对方,应该用 UTF-8 格式解码,但是对方不明白 UTF-8 是什么,只是不停的告诉我他使用的是哪一个 Java 函数。

我的解决方案不敢说万能,但应该即便是上古的解码器都能处理——这个方案就是指定各编码器在编码时,对大于 ASCII 范围的字符均作转义处理为 \uXXXX 格式。这么一通操作后,我的合作伙伴表示:程序通了。

其实在 JSON 规范中,列举了不少篇幅说明大于 U+00FF 的码点应该如何转义,包括大于 U+FFFF 的。所以从笔者的个人观点看来,如果我们严格按照 JSON 规范的话,什么 UTF-8,GB18030 等编码格式都是未被允许的,唯一严格允许的就是 \uXXXX 转义。但是在实际操作中,这种转义太浪费字节序列了,各种语言对 string 类型进行操作时,习惯性地按照本身的字符串在内存中的默认编码格式照搬到 JSON 序列化上了。

如果 JSON 的编码端无法确保或协调对端解码器的编码格式,那么请统一使用 \uXXXX 转义。

JSON 中的 UTF-16

如果读者不需要自行编码或解析 JSON 数据的话,可以跳过这一小节;否则,这一段是必修课。

对于编码值大于 127 的字符来说,如上文所示,我们可以转义为 \uXXXX 格式。那么是不是直接写 sprintf("\\u04X", aByte) 就可以呢?

如果你这么做,那么作为一个通用库来说……

严格来说,\uXXXX 其实是对 UTF-16 编码的转写。这是一个比较少用的编码格式。我们都知道,UTF-8 是一个变长的编码格式,它编码的基本单位是 1 个字节。受早期 Windows 16位 wchar 的影响,有些人可能会误以为UTF-16 是定长的 2 个字节。其实并不然,对于大于 65535 的 unicode 码点,UTF-16 使用 4 个字节编码,而 JSON 只需要将编码后的两个半字(half world)按顺序使用 \uXXXX 转写出来就可以了。

对 JSON 具体需要转义的字符,以及 UTF-16 的相关内容,笔者之前也写过一篇文章专门说明,欢迎移步

ASCII 控制字符

按理说,JSON 只应该承载可见字符。但是按照 JSON 的规范,JSON 承载的是 unicode,而 ASCII 控制字符也是 unicode 的一部分,所以 JSON 也是可以承载 ASCII 控制字符的。

其实这个问题并不大,即便把这些控制字符原封不动地包装在 JSON 序列化之后的数据流中,对端也是可以正确解析出来的。大家要注意的是,如果带控制字符的话,数据渲染到终端时,某些控制字符可能不会被渲染出来。如果此时你从终端复制一段数据,在粘贴到别处,这些字符可能就都丢失了。


老生常谈的浮点数

精度问题

众所周知,在许多语言的内部处理逻辑中,带小数部分的数字是使用浮点数来处理的。对于小数部分无法被 2 除尽的十进制数,系统(为了照顾 “你们人类”)而使用二进制浮点数的近似值来表示。

具体到 JSON 中,坑在哪里?其实吧这里不算是 JSON 的坑,而是一个通用的问题。我简单提一下吧:

首先我们知道,对很多强类型语言来说,浮点数往往可以细分为单精度和双精度两种,前者使用 4 个字节,后者使用 8 个字节。单精度在有效位数方面比双精度数小一大截,但是在具体实践中,考虑到数据传输、计算效率、数值范围,往往单精度就足矣。

这个时候,如果一个浮点数在系统内部经过各种不同精度的转换之后,在转换成 JSON 时会有什么问题呢?我们来考虑一下的过程:

  • 一个十进制精确定点数值 2.1
  • 使用单精度浮点数表示,f = float32(2.1)
  • 调用某些接口,可能接口本身是不支持单精度数,因此转成了双精度处理 d = float64(f)
  • 将这个双精度数填入一个结构体并且格式化为 JSON 小数输出

此时,我们会得到什么数字呢?根据不同的语言,输出可能会不同。如果不指定精度的话,很多 JSON 编码库是支持根据浮点数的具体数值,猜测并且格式化为一个最接近的十进制小数。以 Go 为例子,我们会发现通过 JSON 输出的时候,这个 2.1 变成了 2.0999999046325684

这在本质上,是因为单精度数经过一次类型转换为双精度后,其二进制有效位数以零填充,转为十进制时,对于双精度浮点数,这就不再是双精度有效数字下的 2.1 了。

换句话说,开发者们在处理浮点数时,需要考虑不同精度浮点数的精度处理差异,特别是金融相关的数据计算和传输,一不小心就会造成大量的对账错误。

特殊浮点数

前文提及,JSON 明确说明不支持 +/-Inf 和 NaN 这两组在 IEEE 754 中规定的特殊数值。但有一些数学运算库,在计算之后会将奇点输出为 +/-Inf 或 NaN,对于很多 JSON 编码库来说,遇到这种数值会导致整个数据编码失败。因此开发者需要针对这种情况特殊处理。我开发的 jsonvalue 中就有这样的一个专题。毕竟是笔者在实际操作中趟过的坑……


有顺序的 K-V

在 JSON 规范中,明确强调 array 类型的子值顺序的重要性(这很好理解)。

但是针对 object 类型,key 的顺序则未提及。在实际操作中我发现不少应用场景中把 object 的 K-V 也当作有序数据来操作了——这在很多自己使用代码简单拼接 JSON 串的场景中,出乎意料地很常见。

还请各位明确注意:JSON 的 object,我们应当默认它是无序的。如果需要传递一系列有序的 KV 对,那么请务必使用 array 类型,不要再用 object 了,这绝对不是一个通用的做法。

在这一点上,我自己也犯过一个很低级的错误:

JSON 数据的幂等检查和数据校验

年少无知的我有一次设计过一个模块,接收上游发来的各种事件信息。为了确保事件都被处理,因此当下游响应不及时时,上游可能会将同一事件重复发出。此时我需要对事件进行幂等计算,确保同一事件不会被重复处理。

一开始我这是简单对上游数据进行 hash 计算。但是在实际运作了一段时间,出了 bug,而原因也很简单,我们看看下面两段数据:

  • {"time":1601539200,"event_id":10,"openid":"abcdefg"}
  • {"event_id":10,"time":1601539200,"openid":"abcdefg"}

这两段数据仅仅是 key 的顺序不同而已,但如果使用上面的逻辑,这数据根本就是一模一样的!我们永远要注意:如果我们没有明确地与上游约定好的话,那么请永远不要对上游做任何假设;即便使用文档约束,也依然要多多检查各种例外情况。

结果怎么解决?约束上游?这岂不是显得我能力不行嘛(狗头,主要是不想让上游知道我的 bug 这么 low),所以我在我自己这边简单对解析出来的 key 排序之后(反正 key 不多且无嵌套),再重新计算 hash 来解决。


结语

本文从 JSON 标准出发,结合自己的一些工作经验,整理了 JSON 编解码过程中的一些坑和注意点。如果本文有谬误,还请不吝指正;如果读者还遇到了其他的坑,也欢迎补充。

此外,如果读者中有 Go 开发者的话,也欢迎了解一下我的 jsonvalue 库,点个 star 或者给我提 issue 都非常欢迎~~


参考资料


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,欢迎转载,但请注明出处。

原文标题:《JSON 这么可爱,让我们用千字短文吃透它吧!》

发布日期:2022-10-20

原文链接:https://cloud.tencent.com/developer/article/2137440

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JSON 是什么
    • 关于 JSON 的 “常识”
      • JSON 标准规定了什么
        • JSON 没有规定什么?
        • JSON 的常见 “坑位”
          • 没搞明白编码格式导致解码出错
            • JSON 中的 UTF-16
            • ASCII 控制字符
          • 老生常谈的浮点数
            • 精度问题
          • 有顺序的 K-V
            • JSON 数据的幂等检查和数据校验
        • 结语
        • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档