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 這個類型,但是
true和false被並列為單獨的兩個類型 - 作為最外層的 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