JSON 工具箱

同一份 JSON,四個工具給出四種結果:格式化工具怎麼選

結論:格式化不等於無損:帶前導零、BOM 或大整數的同一份 JSON,經 Python、Node、jq、json.tool 處理後有的報錯、有的靜默改值。10 份樣本實測四條解析路徑,給出按場景選型的對照表與一段可複製的嚴格校驗腳本。

進階約 6 分鐘發佈於 2026-11-03更新於 2026-11-03
四條解析路徑對同一份 JSON 給出不同結果的封面示意

結論:格式化工具之間最要命的差別不是縮進幾個空格,而是同一份輸入,有的報錯、有的悄悄把資料改了。選工具前先拿一段帶邊界值的樣本跑一遍,比看功能列表有用得多。

用戶以為格式化只是「重新排版」,實際上每條路徑都在用自己的解析器重新解釋這份文本。解析器一寬鬆,本該擲出的報錯就變成了靜默改資料,而頁面上依然是一片綠色的「格式化成功」。

一、現象:同一份輸入,四條路徑給出四種結果

實測環境是 Python 3.11.1、Node v22.13.1、jq 1.7,樣本為 10 段手工構造的短 JSON,全部經標準輸入送入、不落盤、不改設定。四條路徑分別是 json.loads、JSON.parse、jq -c .、python -m json.tool。表中 ERR 表示拋例外並中斷,靜默→ 表示不報錯但輸出值與輸入已經不一致。

樣本 python json.loads node JSON.parse jq -c . python -m json.tool
S1 {"a":1,"b":[1,2]} OK OK OK OK
S2 {"a":1,} ERR ERR ERR ERR
S3 {'a':1} ERR ERR ERR ERR
S4 {"a":01} ERR char 6 ERR position 6 靜默→ {"a":1} ERR
S5 {"a":1 /*x*/} ERR char 7 ERR ERR ERR
S6 \ufeff{"a":1} ERR utf-8-sig ERR position 0 靜默→ {"a":1} ERR
S7 {"id":9007199254740993} OK 保真 靜默→ 9007199254740992 OK 保真 OK
S8 {"a":1,"a":2} 靜默→ {"a":2} 靜默→ {"a":2} 靜默→ {"a":2} OK
S9 {"a":1 ERR char 6 ERR ERR EOF ERR
S10 <html>502</html> ERR char 0 ERR Unexpected token '<' ERR ERR

十個樣本裡有四個出現「不報錯但資料變了」:S4、S6、S7、S8。也就是說,只看「格式化成功」這盞綠燈,有四成概率你拿到的已經不是原來那份資料。真正讓人踩坑的是這幾條都不在語法錯誤列表裡——文件能打開、能複製、能導出,錯只在內容。

二、原因:語法層和語義層根本是兩件事

RFC 8259 第 1.2 節說得很直白:ECMA-404 允許若干實踐,而 RFC 為了最大互操作性建議避免;第 4 節要求物件名 SHOULD 唯一,卻沒有規定重複鍵怎麼處理;第 6 節討論數字時只給出互操作建議。ECMA-404 更進一步,明確寫「對數字的語義不可知」,只規定數字序列的表示,不規定語言內部怎麼存。換句話說,規範把大量邊界留給了實現者。

於是「格式化」這一步就分化成兩層:語法層判斷文本是否合法,語義層決定這段文本變成什麼值。前者有標準答案,後者沒有。jq 把 01 讀成 1、把開頭的 BOM 當空白跳過,是它在語義層的選擇;Node 把 20 位整數塞進 IEEE 754 雙精度,是 JavaScript 執行時的選擇;四條路徑對 {"a":1,"a":2} 都取最後一個值,是主流解析器的共同默契。這些都不違反「能解析」,但都改變了資料。

JSON 解析報錯怎麼修:Unexpected token 逐條排查 處理的是語法層,把報錯原文翻譯成坐標;本文處理的是它前面那一步——如果工具壓根沒報錯,你連坐標都拿不到。

三、四類靜默改資料,逐條看它們改了什麼

把四個「靜默」樣本單獨拎出來,改動都很小,小到肉眼掃一遍不會發現:

{"a": 01}                          // 前導零:jq 輸出 {"a":1}
\ufeff{"a": 1}                     // BOM:jq 直接跳過開頭三位元組
{"id": 9007199254740993}           // 20 位整數:JSON.parse 輸出 ...992
{"a": 1, "a": 2}                   // 重複鍵:四條路徑都只留 {"a":2}

前導零和 BOM 屬於「輸入本來就不合規,但工具替你寬容了」。01 在標準 JSON 裡非法(RFC 8259 第 6 節禁止前導零),BOM 也不該出現在 JSON 文本開頭;Python 與 Node 都明確報錯,jq 卻選擇繼續。這類寬容在日誌清洗時很方便,在契約校驗時是災難:上遊以為自己發的是非法資料需要修,下遊已經拿到一份「看起來正常」的結果。

大整數屬於「輸入完全合規,是執行時裝不下」。9007199254740993 是合法 JSON 數字,但超過 2^53-1,JSON.parse 只能表示成 9007199254740992。Python 的 int 與 jq 都保住了原值。如果這份 JSON 裡是訂單編號或雪花 ID,末位被改一位就意味著對不上帳。

重複鍵屬於「規範沒規定,實現各自選」。四條路徑全部靜默取後者,沒有任何一條給出警告。RFC 8259 只說物件名 SHOULD 唯一,所以這不是 bug,但它是拼接設定、合併修補時最難查的一類問題:《從 RFC 8259 讀懂 JSON 語法邊界》對這類「允許但不建議」有更細的展開。

四、報錯定位能力:誰給行列,誰只給一句

再看出錯的六個樣本,四條路徑給出的資訊量差別很大。Python 的 JSONDecodeError 同時給 msg、lineno、colno、pos;Node v22 的報錯帶 position N (line 1 column M) 並附上出錯處的原文片段;python -m json.tool 復用 Python 的資訊;jq 只給一句 parse error: ... at line N,不給字元偏移。

單行壓縮 JSON 上,行列號基本失效——所有內容都在 line 1。這時候要的是字元偏移,下面這段指令碼把校驗統一成「報錯 + 偏移 + 上下文」:

import json, sys

def strict_check(text: str) -> None:
    try:
        json.loads(text)
        print("OK")
    except json.JSONDecodeError as e:
        print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
        lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
        print("...", text[lo:hi].replace("\n", "\\n"), "...")
        sys.exit(1)

strict_check(open(sys.argv[1], encoding="utf-8").read())

它比 jq . 嚴格(會拒絕前導零與 BOM),而且給出 e.pos。具體怎麼把 line 1 column 23611 換算成位元組位置、中文文檔裡字元偏移和位元組偏移為什麼會差三倍,單行 JSON 報錯怎麼定位:從 line 1 column 23611 找到出錯的那個位元組 已經講透,這裡不重複。

五、怎麼改:把格式化拆成「嚴格校驗」+「美化」兩步

結論很簡單:不要用一個「格式化」按鈕同時承擔校驗與美化。先用嚴格解析器判合法性,再決定用什麼工具排版。一行命令就能把第一步獨立出來:

python3 -m json.tool input.json > /dev/null && echo "語法 OK" || echo "語法不合法"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json

第一步只回答「能不能解析」,第二步再檢查「解析出來是不是你期望的結構」。若資料裡有大整數,改用 Python 或 jq 讀取,別走 JSON.parse;若源資料可能帶 BOM,讀入時顯式指定 encoding='utf-8-sig' 而不是指望工具自動跳過;若鍵可能重複,得在解析前單獨掃一遍,因為四條路徑都不會提醒你。把這幾條寫進 CI,就是 編輯器、命令列與自動化工具鏈 講的流程,只是多了一層資料保真斷言。

六、怎麼選:四條路徑的選型表

路徑 強項 風險 適合
python json.loads / json.tool 嚴格、給行列與字元偏移、整數不丟精度 拒絕前導零與 BOM,需顯式處理編碼 契約校驗、CI、日誌排查
node JSON.parse 瀏覽器原生、報錯附帶原文片段 大整數靜默截斷、BOM 報錯 前端調試、瀏覽器內工具
jq -c . 管道友好、最快出結果 前導零與 BOM 靜默通過、不給字元偏移 運維管道、快速取欄位
瀏覽器格式化工具 /parse、/editor 樹形瀏覽、鍵排序、複製路徑、無需裝環境 行為取決於網頁用的解析器,必須先驗一次 人工檢視、臨時除錯

選型的關鍵不是「哪個功能多」,而是「它對我的資料會不會撒謊」。帶訂單編號、金額、設備 ID 的資料,優先 Python 或明確聲明保真的工具;只想看結構、不落庫的調試,瀏覽器工具最省事。本站 /parse 走的是瀏覽器內的語法校驗與格式化,適合「先看清楚長什麼樣」,但涉及大整數時仍建議按上面的指令碼覆核一次。

七、隱私只是其中一條邊界

「資料會不會上傳」是選在線工具時最常被問的一條,但它和「資料會不會被改」是兩件獨立的事:本地執行、斷網可用的工具,照樣可能把 20 位整數截斷;反過來,一個嚴格報錯的在線工具,也可能把原文 POST 到伺服器。兩條要分別驗證,不能互相替代。《在線 JSON 格式化會上傳資料嗎》那篇給了三個兩分鐘能驗完的動作;本文補的是另一半——拿下面這份樣本清單,確認工具不會靜默改值。

八、一份可複製的自檢清單

把 S1–S10 這十段存成一個 cases.txt,每行一段,貼進任何候選工具,核對三件事:報錯的樣本是否真的報錯、報了錯是否給到具體位置、沒報錯的樣本輸出是否與輸入逐字一致。特別核對 S4、S6、S7、S8 四行——只要有一行「靜默→」,就說明這條路徑不能單獨用來做資料校驗。

再補三條工程約束:把嚴格校驗放進提交前勾子,別等上線才發現大整數對不上帳;多語言環境下統一用 UTF-8 無 BOM 寫文件;對來自第三方的 JSON 保留原始回應內容,格式化後的結果只用於閱讀,不用於回寫。《編輯器、命令行與自動化工具鏈》裡那套 pre-commit 設定可以直接接上這份清單。

常見問題

格式化通過,是不是就說明 JSON 沒問題?

不是。S4、S6、S7、S8 四個樣本都「通過」了格式化,但前導零、BOM、20 位整數和重複鍵都被改寫或吞掉。格式化只證明文本能被某個解析器接受,不證明解析後的值等於你發出的值;關鍵欄位要單獨做逐值斷言。

為什麼 jq 會接受 Python 報錯的前導零和 BOM?

jq 有自己的詞法分析器,對 01 這類數字採取寬鬆策略,也會跳過開頭的 BOM;Python 與 Node 按 RFC 8259 嚴格拒絕。這不是版本 bug,是「容錯解析器」與「嚴格解析器」的取捨。做清洗時前者省事,做契約校驗時必須用後者。

大整數被改成 9007199254740992,怎麼避免?

JavaScript 的 Number 是 IEEE 754 雙精度,安全整數上限是 2^53-1。用 JSON.parse 讀 20 位 ID 必然截斷。改用字元串保存這類 ID,或改用 Python int、jq、以及支持 BigInt 的解析庫;前端確實需要數值時,在解析前把該欄位替換成字元串再處理。

在線工具和本地命令行,到底該用哪個?

按資料敏感度與用途分:只是看結構、找欄位,瀏覽器工具最快;要進流程、要斷言資料保真,用 python -m json.tool 加嚴格校驗指令碼。兩者都要先跑一遍 S1–S10 樣本,確認報錯與輸出符合預期,再決定輸入哪一類資料。