JSON 檔案格式
JavaScript Object Notation,輕量的結構化資料格式。
- MIME 類型
- application/json
- 副檔名
- .json
- 分類
- 文字與標記語言
Magic number(檔案簽章)
以 '{' 或 '[' 開頭、後面接帶引號的鍵或巢狀括號的文字。
關於 JSON 格式
JSON 是 JavaScript 物件字面值語法的一個子集,Douglas Crockford 做的是把它寫下來而不是發明它,而整份規格輕鬆放得進幾頁紙。它有六種型別——物件、陣列、字串、數字、布林與 null——沒有日期、沒有二進位、沒有註解、也沒有版本。這些缺席每一項都是刻意的,而每一項也都有人在繞:日期變成 ISO 字串、二進位變成 base64、註解變成一個沒人會讀的鍵、版本控制變成一個讀取端得記得去檢查的欄位。極小的文法加上無所不在的支援,是它勝出的原因;而幾乎每個實務上的 JSON 問題,都出在這個格式沒有規定的那些東西身上——原因也是同一個。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 1 | First structural character | 去掉開頭空白之後,物件是 7B、陣列是 5B。現行標準允許 JSON 文字是任何一個值,所以單獨一個數字或一個帶引號的字串也合法,但已被取代的 2006 年規格只允許物件與陣列,而現存幾乎每個產生端吐的還是這兩種之一。 |
| 0 | 4 | Encoding-detection window | 因為 JSON 文字的前兩個字元一定是 ASCII,前 4 個位元組裡 null 的分布就能認出編碼:00 00 00 xx 是 UTF-32 大端序、00 xx 00 xx 是 UTF-16 大端序、xx 00 00 00 是 UTF-32 小端序、xx 00 xx 00 是 UTF-16 小端序,而四個都非 null 就是 UTF-8。現代 JSON 在傳輸上必須是 UTF-8,所以實務上這個測試告訴你的是「哪裡出錯了」,而不是「接下來該怎麼做」。 |
| 0 | 3 | UTF-8 byte-order mark | EF BB BF。標準說實作不得加它,並允許解析器忽略它;Node 和瀏覽器背後的 JavaScript 引擎兩件事都沒做,直接在它上面丟例外——所以 Windows 編輯器加上去的標記,是手改過的設定檔明明文字看不出任何變化卻突然載不起來的家常原因。 |
| 3 | 1 | First structural character behind a mark | 有標記時,文件真正開始的地方。值得單獨拿出來講,因為解析器報的錯誤會說位置 0 有非預期的 token,而位置 0 那個字元在任何你會拿去找它的編輯器裡都是隱形的。 |
| 0 | variable | Everything else | 沒有標頭。沒有 magic number、沒有長度、沒有版本、沒有檢查碼、沒有尾標、沒有索引。JSON 檔案從第一個位元組到最後一個都是內容,而讀取端可能想知道的每一件事——它多大、它完不完整、它是什麼形狀——都只能靠整份解析完才拿得到。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| NDJSON/JSON Lines | 開頭逐位元組相同——兩邊都是一個左大括號。差別在 JSON Lines 每一行放一份完整文件,外面不包陣列、彼此之間也沒有逗號,所以標準解析器讀完第一行很順利,然後在第二行卡在「JSON 結束之後出現非預期字元」。日誌管線與資料集傾印用它,正是因為它可以一直往後追加、也可以一行一行讀。 |
| JSONC / JSON5 | 位移 0 一樣相同,然後在第一個 // 註解或結尾逗號出現的瞬間就不合法了。tsconfig.json 和好幾套編輯器的設定檔是 JSONC,不是 JSON——所以用嚴格解析器去讀它們的程式會失敗,而寫出這些檔案的編輯器連一句話都沒說。 |
| XML | 一個大括號或方括號對上一個角括號,分辨起來毫無難度。真正值得比的是它們各自的用途:JSON 表達清單只有一種方式,而且完全沒辦法表達「內含標記的文字」,這就是為什麼資料搬到了 JSON,文件沒有。 |
| CBOR/MessagePack/BSON | 資料模型幾乎相同的二進位編碼,而且它們都不是文字。CBOR 把 map 寫成一個落在 A0 到 BF 之間的開頭位元組;BSON 開頭是一個小端序的 32 位元總長度,所以它的第一個位元組通常很小、不可列印。如果一個本該是 JSON 的檔案打開來是二進位,答案很可能是其中之一,而且什麼都沒有壞。 |
| JSONP | 物件前面多了一個回呼函式名稱和一個左括號——那是一段可執行的 JavaScript 敘述,不是資料文件。規則簡單到看第一個位元組就能套用:以字母開頭的東西不是 JSON。 |
常見的壞法
- 被截斷。沒有長度、也沒有尾標,所以被切掉的文件只會以「輸入非預期結束」失敗,而且無從得知少了多少。沒有做最後一次 flush 的串流,掉的就只是那個右大括號,這也是為什麼最常奏效的修法就是把它補回去。
- 位元組順序標記。三個沒有編輯器會顯示的位元組,卡在一個其他部分完美無缺的檔案前面,然後產生一個指名位置 0 的解析錯誤。在 hex 檢視器裡看一下前三個位元組,幾秒鐘就解決一件否則要耗掉一個下午的事。
- 後面有多餘資料。兩份文件被串在一起,或是有人往檔案後面追加了一行日誌,會得到「JSON 之後出現非空白字元」以及一個位置——而那個位置是以解碼後文字的字元數計算的,不是位元組數,所以只要檔案裡有非 ASCII 內容,它就對不上 hex 編輯器的游標。
- 數字讀回來變成另一個數字。這個格式只有一種數字型別、也沒有規定精度,而建立在 IEEE 754 倍精度上的讀取端,只有在負 2 的 53 次方加一到 2 的 53 次方減一之間的整數才完全一致。識別碼 9007199254740993 讀進來再寫出去會變成 9007199254740992,過程中沒有任何地方報錯:磁碟上的位元組是對的,記憶體裡的值不是。踩過這個坑的系統,從此都改用字串傳識別碼。
- 落單的代理對字元。字串裡有一個像 \uD800 這樣的逸出、後面卻沒有配對的逸出,照樣解析成功,因為文法只要求四個十六進位數字;然後它寫不回合法的 UTF-8。另一端跑出來的是一個替代字元,於是在每一步都回報成功的來回之後,資料悄悄地變了。
這個格式現在還在用嗎
只要沾到網路,JSON 就是預設的資料格式,而且這個位子沒有受到認真挑戰。REST 和 GraphQL 都說它,套件資訊檔與鎖定檔用它寫,日誌管線一行吐一份文件,而每種語言的標準函式庫都附了解析器。比起「它用在哪」,「哪裡刻意不用它」更說明問題。設定檔一路往 YAML 和 TOML 漂移,因為 JSON 沒有註解、也不准結尾逗號,這些是使用體驗的抱怨而不是技術問題,但一樣具有決定性。高流量的內部通訊搬去 Protocol Buffers、CBOR 或 MessagePack,因為解析文字比解析帶長度前綴的二進位貴,而且有 schema 值得多一個建置步驟。超大量的表格資料交給 Parquet,因為 JSON 每一列都要把每個鍵再寫一次。讓 JSON 保住首選地位的,是人讀得懂它:要給別人串接的 API、有人會在凌晨三點手改的設定檔,還有任何靠肉眼看就要除錯的東西——那點效率損失,不過是「在終端機裡能自己說明自己」的代價。
在這個站上打開 JSON 檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 JSON 搞混的格式,可以比對它們的開頭位元組。