博客 / 詳情

返回

JSON 這麼可愛,讓我們用千字短文吃透它吧!

JSON,一個偉大的協議,前端工程師的卓越發明!相信 99% 的程序員都認識 JSON,它作為前後端交互的熱門協議,因其易理解、簡單、靈活和超強的可讀性,得到了互聯網的廣泛歡迎,甚至很多微服務之間的傳輸協議中也得到應用。

但是筆者在開發一個 Go 的 JSON 編解碼庫的過程中,除了自己趟過各種奇奇怪怪的問題之外,也認識到廣大程序員們對 JSON 各種奇奇怪怪的用法和使用姿勢。在處理解決這些問題之後,筆者萌生了對 JSON 進行進一步科普和介紹的想法。

相信我,看完這篇文章,你就可以吃透這個可愛的 JSON 了。

這不是萬字長文,所以答應我,不要 TLDR(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) 就可以呢?

如果你這麼做,那麼作為一個通用庫來説……

<img src="https://ts1.cn.mm.bing.net/th/id/R-C.f1ca9cac65236fb8944862661d3a915c?rik=3iTDjVXAAsd2lw&riu=http%3a%2f%2fps3.tgbus.com%2fUploadFiles%2f201206%2f20120619174916338.jpg&ehk=wYBPVaiaHHF33Liake9fWy32femV5sBtaHR4BkfWcYs%3d&risl=&pid=ImgRaw&r=0&sres=1&sresct=1" width="25%" height="25%">

嚴格來説,\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 都非常歡迎~~


參考資料

JSON 相關資料:

  • Python JSON模塊解碼中文的BUG
  • 既然 GB18030 可以是 Unicode 的 UTF 轉寫,為什麼中國境內不強制使用該字符集?
  • Go json 踩坑記錄
  • axgle/mahonia
  • golang:gbk/gb18030編碼字符串與utf8字符串互轉
  • GB 18030 根上跟 Unicode 有關係嗎?
  • 細説:Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4
  • Unicode
  • UTF-16
  • GB 18030

本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

原作者: amc,原文發佈於雲+社區,也是本人的博客。歡迎轉載,但請註明出處。

原文標題:《JSON 這麼可愛,讓我們用千字短文吃透它吧!》

發佈日期:2022-10-21

原文鏈接:https://segmentfault.com/a/1190000042660224

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.