從一次網路請求說起
場景大概是這樣:你正開著瀏覽器的開發者工具,Network 分頁裡躺著一條請求,點開 Response,一堆擠在一起的文字瞬間佔滿整個螢幕。沒有縮排、沒有註解、每個鍵名都框著雙引號,乍看之下像一個喝醉的人寫的 JavaScript。但你認得出來——這是 JSON,這年頭幾乎所有 API 回傳的預設長相。
我記得自己第一次打開 API 的回應時,心裡的感想是:「就這?」一個方括號,裡面幾十個花括號,每個都長得一模一樣。第一印象說不上驚豔,但後來我慢慢學會一件事:無聊往往是好事的徵兆。
另一種場景是兩個系統在幕後交換資料。前端按下「送出」,瀏覽器把資料序列化成一串字元丟給伺服器;伺服器解析完、處理好,再序列化一份回傳。你永遠不會親眼看到這些字元,但它們一天二十四小時在世界各地這樣互相喊話,而且用的幾乎都是同一種格式。你想看它們長什麼樣,唯一的方法是去翻 Log,或者像我一樣,打開 DevTools 盯著那條請求發呆。
JSON 到底是什麼
JSON 的全名是 JavaScript Object Notation,中譯「JavaScript 物件標記法」。想出這個格式的人是 Douglas Crockford,時間大約在 2001 年。據說起因是他在網路上跟人討論資料交換格式,對當時的主流選擇 XML 頗有微詞,覺得那東西太笨重,於是直接拿 JavaScript 的物件字面值(object literal)當靈感,訂出了一個「人類和機器都能讀」的輕量文字格式。
名字跟 JavaScript 掛鉤,其實是這件事裡最倒楣的部分。因為 JSON 除了語法有血緣關係之外,跟 JavaScript 沒有半點實質關係——它不依賴任何 JavaScript 執行環境,每一種現代語言都有解析它的函式庫。甚至可以說,JSON 作為一種「語言」本身是死的,而死得愈徹底,就愈穩定。它只是借用 JavaScript 的物件寫法,描述了一份「資料應該長什麼樣」的約定。
這份約定後來被正式標準化了好幾次:JSON 本身的語法定義在 ECMA-404;資料交換層面的規範則寫在 RFC 8259,於 2017 年發布,取代了更早的 RFC 7159 與 RFC 4627。若你想看權威定義,官方網站是 json.org——一個幾乎沒有 CSS、從 2000 年代初期一路用到現在的純文字網頁,上面就一張圖、幾段話,二十年沒怎麼變過。某種程度上,這本身就是這個格式最誠實的自我介紹。
為什麼是它贏了 XML
現在回頭看,活在 XML 時代的人是真的辛苦。那時候要接 web service,主流是 SOAP,一個最簡單的請求長這樣:
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<CreateUserRequest>
<User>
<Name>Alice</Name>
<Email>alice@example.com</Email>
</User>
</CreateUserRequest>
</soap:Body>
</soap:Envelope>
這還只是冰山一角。真正開始維護以後,問題才一個一個浮出來:某個欄位到底該用元素還是屬性?<User Name="Alice"> 還是 <User><Name>Alice</Name></User>?命名空間(namespace)怎麼弄?各家 XML Schema 的寫法為什麼還不一樣?最後為了看懂這些東西,你往往還得再學一套 XSLT,用一份 XML 把另一份 XML 轉換成第三份 XML——你沒看錯,就是用 XML 處理 XML,處理完再去處理 XML。設定檔讀起來像一本電話簿,看半天不知道重點在哪。
JSON 會贏,與其說它漂亮,不如說「其他選項更煩人」。它做對了幾件事:體積小,沒有滿坑滿谷的重複標籤;它對應的資料結構,每一種語言本來就內建——dict、hashmap、object,list、array,字串、數字、布林、null,幾乎是語言內建型別的一比一對照表;你不必先讀一份 schema 才能看懂資料,光看內容就知道它想表達什麼;學習成本趨近於零,五分鐘就能上手,寫錯的機率也低。
這其實不是一場公平的比賽。XML 背著二十年的歷史包袱在跑,JSON 什麼都沒帶,當然跑得快。後來 XML 的後繼者們想東山再起,已經太晚了——開發者早就被 JSON 慣壞了。
六種資料型別
JSON 全部只有六種型別,一次列完:
- 物件(object):花括號包起來的鍵值對集合,
{"name": "Alice"}。 - 陣列(array):方括號包起來的一串值,
["a", "b", "c"]。 - 字串(string):一定用雙引號,支援跳脫字元,
"你好,世界"。 - 數字(number):整數、小數、指數都行,
42、3.14、6.02e23,但沒有 NaN、沒有 Infinity。 - 布林值(boolean):只有小寫的
true和false。 - null:就是 null,代表「沒有值」,跟 JavaScript 的
undefined不是一回事。
就這六種。沒有日期、沒有函式、沒有 undefined、沒有二進位。任何一種程式語言的資料結構,最終都能拆解成這六種型別的組合——這正是它通用的根本原因。
來一個真實感多一點的例子,一份使用者紀錄大概長這樣:
{
"id": 1024,
"name": "陳小明",
"email": "ming@example.com",
"isAdmin": false,
"score": 87.5,
"tags": ["程式", "前端", "JSON"],
"lastLogin": null,
"address": {
"city": "台北",
"zip": "100"
}
}
一份登入紀錄(log line)又是另一種長相,通常塞在陣列裡一行一筆:
[
{"time": "2026-08-12T09:30:00Z", "level": "info", "msg": "login ok", "uid": 1024},
{"time": "2026-08-12T09:31:02Z", "level": "error", "msg": "timeout", "uid": 1024}
]
有沒有發現?不管這份資料是出自哪個團隊、哪種語言,只要看它一眼,你馬上就知道結構。這就是它最強的地方:沒有歧義。
那些容易踩的語法坑
JSON 的規則出了名的嚴格,嚴格到第一次寫的人會成群結隊地踩到同一批坑。最經典的就是:鍵名一定要加雙引號。JavaScript 裡寫 {name: "Alice"} 完全合法,放進 JSON 就是錯的,必須是 {"name": "Alice"}。字串只能用雙引號,單引號一律不收。沒有註解——寫了就是語法錯誤。結尾不能有逗號,[1, 2, 3,] 是錯的,花括號同理。數字不能有前導零,007 不合法;小數點前面一定要有數字,所以 .5 是錯的,0.5 才是對的。至於 -0,語法上其實合法,但很少有人真的想寫它,寫了也只是給自己找麻煩。
重複的鍵名在 JSON 裡不算錯誤,{"a": 1, "a": 2} 不會報錯,但行為未定義——絕大多數解析器會保留最後一個值。這意味著如果你在併入多份資料時沒檢查重複,就可能默默拿到錯誤的結果,而且沒有任何錯誤訊息提醒你。
Unicode 字元可以用 \uXXXX 跳脫表示;超過基本多文種平面(BMP)的字元,例如 emoji,若要用跳脫寫法就必須拆成代理對(surrogate pair)——不過直接照原樣寫進文件裡也完全合法。完整的跳脫字元清單是:\"、\\、\/、\b、\f、\n、\r、\t、\uXXXX。其中 \/ 在 JSON 裡其實可以不跳脫,標準只是允許,很多產生器仍會加上,因為這樣序列化結果可以直接塞進 HTML 的 <script> 標籤而不至於誤關標籤。
這些規則囉嗦嗎?囉嗦。但正是因為囉嗦,解析器才能寫得又快又簡單,你也幾乎不用猜一份文件到底合不合法——規則白紙黑字,沒有灰色地帶。嚴格,在這裡是特徵,不是 bug。
JSON 是 JavaScript 的子集嗎
嚴格來說,不是。JSON 是「JavaScript 物件字面值語法」的遠親,但兩者絕不是一回事。
JavaScript 裡合法的物件寫法,JSON 通通不收:單引號字串、結尾逗號、註解、函式、undefined、NaN、Infinity、Date 物件……這些在 JSON 裡連影子都看不到。反過來說,只要一份文件能通過 JSON.parse,它原則上也是一段合法的 JavaScript 表示式——除了某些角落的例外。
歷史上有個著名的插曲。最早的規範 RFC 4627 白紙黑字宣稱 JSON 是 JavaScript 的子集,後來這個說法被發現不太站得住腳。最有名的例子是 U+2028(行分隔符)和 U+2029(段分隔符)這兩個字元:在 JSON 字串裡它們完全合法,但在舊版 JavaScript 的語法裡,它們會被當作換行處理。結果就是,你用 <script> 內嵌一份含這兩個字元的 JSON 時,JavaScript 解析器直接崩潰。一直到 ES2019 引入了所謂的 JSON 超集(JSON superset)提案,才把這兩個字元從 JavaScript 字串的違法清單裡移除。所以現在比較正確的說法是:自 ES2019 起,JSON 已經正式成為 JavaScript(ECMAScript)的子集,原本 U+2028/U+2029 那處角落差異已被消除。
為什麼要在意這個差別?因為它決定了你該用什麼工具解析。正解永遠是 JSON.parse 或對應語言的函式庫,而不是把字串丟進 eval 或 Function()——下一節要講的,正是這個選擇曾經付出過的代價。
每天都會遇到的場景
說 JSON 是現代開發者最常接觸的「格式」,一點都不誇張。先數最常見的:
- REST API:幾乎所有現代 API 的請求與回應 body 都是 JSON。你送出一份,收到一份,中間伺服器做了什麼你未必看得到,但兩端認的是同一種格式。
- 設定檔:
package.json、tsconfig.json、.eslintrc.json,還有一堆你叫不出名字的工具預設值。以前這種位置是 XML 或 INI 的天下,現在 JSON 幾乎是預設。好笑的是,JSON 沒有註解,偏偏設定檔最需要註解——這個矛盾我們留到後面再算帳。 - 瀏覽器儲存:
localStorage只能存字串,所以任何稍微複雜的資料都得先JSON.stringify存進去,下次再JSON.parse讀回來。 - 資料庫:PostgreSQL 有
jsonb型別,可以直接查詢 JSON 欄位;MongoDB 更徹底,整個資料庫的文件格式 BSON 就是 JSON 的二進位變體。 - 日誌與串流:JSON Lines(NDJSON)每行一個獨立的 JSON 值(通常是物件),適合串流處理與 log。一行一筆,斷在哪一行都不怕,
tail起來特別親切。 - WebSocket:即時通訊的訊息格式,幾乎清一色是 JSON。
- 瀏覽器的 fetch:
fetch(url).then(r => r.json()),一行拿 JSON,一行送 JSON。
舉個生活化的例子。你在網頁上看到購物車的小紅點數字,背後八成是前端把 cart 物件 JSON.stringify 進 localStorage,重新整理後再 JSON.parse 回來。你在聊天室看到訊息即時冒出來,背後是伺服器廣播了一則 JSON 到你的 WebSocket。你在終端機追一條錯誤 log,看到一行擠在一起的 JSON——那就是你要找的記錄。JSON 就是這樣滲透進每天的日常,低調到你幾乎不會特別想到它。
各種語言怎麼解析
每種語言都有自己的慣用法。我不打算寫教學,只想讓你看一眼各家的長相,感受「同一種格式,每個語言都有對應的處理方式」這件事。
JavaScript 原生支援,出生就內建:
const obj = JSON.parse(raw); // 字串 → 物件
const str = JSON.stringify(obj); // 物件 → 字串
Python 用 json 模組,命名直白好記:
import json
obj = json.loads(raw) # 字串 → dict
str = json.dumps(obj) # dict → 字串
Java 世界則是第三方程式庫的天下,主流是 Jackson 或 Gson,可以在 JSON 與你手上的 POJO 之間互轉:
User user = objectMapper.readValue(raw, User.class);
String str = objectMapper.writeValueAsString(user);
Go 標準庫的 encoding/json 用結構體標籤(struct tag)對映欄位,型別安全,但缺欄位、型別不匹配這類雷需要一段時間才摸得熟:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
Rust 生態則以 serde 為主流——它不只解析 JSON,而是定義了一套通用的序列化框架,JSON 只是其中一個後端:
let user: User = serde_json::from_str(raw)?;
可以看到,語言風格差異很大:有的內建、有的靠生態系、有的靜態型別安全、有的動態靈活,但每一家對「JSON 長什麼樣」的理解完全一致。這就是格式標準化的意義——一種真正的共通語言。
安全:那些用血換來的教訓
JSON 今天能有一套像樣的安全底子,是踩過無數坑換來的,而且很多坑是用實際的漏洞與資料外洩堆出來的。
最古老的一課是 eval。在 JSON.parse 普及之前,網路上隨處可得的教學是「用 eval 解析 JSON」。聽起來很聰明:把字串丟進 eval,它就把它當程式碼執行,直接變成物件。問題在於,eval 執行的是程式碼,不是資料。你以為你在解析一份 JSON,其實你在執行攻擊者準備好的程式。早期的寫法往往是 eval("(" + responseText + ")"),攻擊者只要能在回應字串裡塞進一小段惡意程式碼,就等於拿到了在你頁面上執行任意程式碼的鑰匙——那正是遠端程式碼執行(RCE)。後來大家終於學會:解析未經驗證的 JSON,永遠要用真正的解析器,eval 這條路想都不要想。
JSONP 是另一個妥協的產物。在 CORS 出現之前,跨網域請求被同源政策擋得死死的,JSONP 的做法是把 JSON 包在 <script src> 裡回傳:伺服器回傳的不是純資料,而是一段「呼叫某個全局函式」的 JavaScript,等於把資料塞進函式呼叫裡送到你的頁面。它確實繞過了限制,但也把「讓別人執行你的程式碼」當成預設前提,一旦來源不可信,就是引狼入室。CORS 成熟之後,JSONP 就該好好退休了。
還有 CSRF 的大洞。早期的規範允許 JSON 最外層是陣列,這引出一個問題:如果伺服器在 /api/users 回傳一份陣列,攻擊者可以用 <script src="https://victim.com/api/users"> 直接載入它——因為陣列是合法的 JavaScript 表示式,瀏覽器會真的去執行它——再透過覆寫陣列原型的方法把資料偷走。這就是所謂的「陣列劫持」(array hijacking)。業界的防禦方式很直白:把最外層包成物件,{"data": [...]},因為裸物件在 <script> 環境裡取不到值。一直到今天,很多服務仍然堅持這個慣例。有趣的是,後來的 RFC 7159 反而放寬了頂層限制,允許任何值放在最外層,並且在安全考量裡把話說得很明白:這個問題的本質在於「資料被當成 JavaScript 執行」,而不是格式本身有錯。所以要防的,是永遠不要把未經驗證的 JSON 交給 JavaScript 執行環境去求值。
JSON 的安全課還有兩堂。其一是原型污染(prototype pollution):很多程式庫在「合併」兩份 JSON 物件時,會直接把攻擊者送來的 __proto__ 鍵複製到原型上,從此整個頁面的物件都繼承了這些屬性。就算你的解析器本身安全,只要下游有一個天真的 merge 函式,就還是會中招——所以現在挑函式庫,要特別避開那種會把資料併進原型物件的設計。其二是 JSON 炸彈:極度巢狀的結構 [[[[[[...]]]]]] 或超大陣列,能讓一個看似只有幾 KB 的請求,在解析時耗光伺服器的記憶體與 CPU,是非常有效的窮人版 DoS。現代的最佳實務總結起來就四件事:永遠用真正的解析器、永遠不碰 eval、限制 payload 大小、避開會污染原型的函式庫。
校驗與擴充:JSON Schema 與 JSON5
JSON 本身只定義語法,不定義「這份資料應該長什麼樣」。於是有了 JSON Schema——一套用 JSON 自己寫的「資料說明書」:
{
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"}
}
}
用它可以自動檢查一份文件合不合規、欄位型別對不對、哪些欄位必填。OpenAPI(前身是 Swagger)就把整套 API 描述建立在它之上。JSON Schema 的細節多到可以寫好幾本書,但核心概念很簡單:給資料一份可以自動驗證的契約,讓錯誤在進入系統之前就被攔下來。
另一方面,JSON 的嚴格語法對人類實在太不友善,於是出現了一批「規則放寬版」的變體格式。最知名的是 JSON5,它把 JSON 拒收的東西又全部放了回來:註解、結尾逗號、不帶引號的鍵名、單引號字串,甚至 Infinity 和 NaN 都行。寫起來很愉快,代價是它不再是標準 JSON,必須靠額外函式庫處理。JSONC 更克制,就是「JSON 加上註解」;HJSON(Human JSON)主打人類可讀,理念類似。
這三個都不是標準,也永遠不會成為標準——它們存在的意義,就是填補「JSON 不給寫註解」這個洞。老實說,一個格式需要靠「寬鬆版」才順手,本身就說明了原設計在某些情境下有多不通人情。但這也帶出一個殘酷的事實:註解的需求明明如此真實,標準卻偏偏不給你,你只能自己想辦法。
它的限制與槽點
接著,把該抱怨的一次說完整。JSON 的缺點其實都是同一批,十幾年沒變過。
最常被唸的是沒有註解。設定檔最需要註解——「這個值在幹嘛」「為什麼要設成 3」「別改這個,會壞掉」——偏偏 JSON 一個都不准。於是你看見 package.json 的維護者發明 "_comment" 這種虛擬鍵,或是在 README 裡寫長篇說明,或乾脆改用 JSONC。這是一個非常真實、每天都在發生的困擾。
沒有日期型別。日期只能用字串表示,於是大家各自發明 ISO-8601 字串的寫法:時區要不要帶、帶 Z 還是帶 +08:00、毫秒要不要留,每個團隊都有自己的版本。為了這件「小事」,date-fns、Joda-Time、java.time 這些函式庫才會如此重要——因為字串永遠不會自己變成時間。
數字精度是更硬核的問題。JSON 數字走的是 IEEE-754 雙精度浮點數,超過 2^53 的整數就無法精確表示。一般網站無感,但銀行、金流系統在 JSON 裡傳金額就得非常小心,一個位數的誤差都是錢。很多系統的解法是乾脆把金額當字串傳,寧可犧牲一點優雅,換來精確。
沒有二進位。檔案、圖片、影片要進 JSON,必須先 base64 編碼,體積暴增三分之一,還要多花 CPU 做編解碼。真要傳檔案時,這體驗就很難受。加上巢狀太深可能把解析器的呼叫堆疊炸掉、重複鍵行為未定義、每個新專案都要重新發明「日期怎麼寫」的輪子——這些槽點單看都很小,加在一起,就是很多工程師寧願改用其他格式的理由。
替代品們
講替代品,不能不誠實地比較一下各家。
YAML 是最常被拿來跟 JSON 比的。它的人類可讀性最好,設定檔寫起來乾淨,還支援註解和多行字串。但 YAML 的規則出了名的難搞:縮排敏感、yes/no 在某些解析器裡會被解讀成布林值、錨點與別名(anchor/alias)的語法像天書、Tab 與空白的戰爭永無寧日。如果你經歷過一次版本號 version: 1.0 被默默解析成浮點數、version: 1 被解析成整數的驚喜,你就懂那種痛苦。YAML 的「方便」,往往是拿「嚴格」換來的,而它的嚴格還不是 JSON 那種規則分明的嚴格,而是充滿歧義的嚴格。
TOML 是設定檔的專門解,支援註解、型別清楚、語法簡單,新一代工具(例如 Cargo 的 manifest、不少新專案)都偏愛它。代價是表達能力有限,複雜的巢狀結構寫起來很痛苦,不太適合當通用的資料交換格式。
Protocol Buffers(Protobuf)走的是另一個極端:先定義 .proto schema,再用 codegen 生成各語言的程式碼,序列化結果是緊湊的二進位,又快又小、型別安全。但代價是要先寫 schema、要跑 codegen、出問題時打開檔案是一堆亂碼,人類無法直接閱讀。適合伺服器之間高流量的內部通訊,不適合當公開的檔案格式。
MessagePack 和 CBOR 可以理解成「二進位的 JSON」:把同樣的資料壓成更小的二進位表示,語意幾乎與 JSON 一一對應,卻有型別與效能上的優勢。BSON 則是 MongoDB 自家的變體,多了日期、二進位、正則表示式等型別,代價是體積比純 JSON 略大。
什麼時候該用誰,沒有標準答案,但可以給一個粗糙的判斷:要給人讀、要公開交換,選 JSON;要機器間高速交換,選 Protobuf 或 MessagePack;寫設定檔且在意可讀性,選 TOML;想省麻煩而且只有你能控制讀取端,再考慮 YAML——並且做好心理準備。
結語:無聊的格式才活得久
二十五年前,JSON 只是 Crockford 某天隨手訂出來的小東西,如今它幾乎是所有程式語言的共同起點。它不漂亮、不新潮、沒有炫技的功能,但它就是活得好好的。
它贏在哪裡?夠用、無所不在、簡單到無聊。任何一個新進的開發者,五分鐘就能看懂一份 JSON;任何一種語言,都有成熟的函式庫處理它;任何一套工具鏈,都把它當成預設格式。當一個格式被所有人當成「理所當然」時,它就贏了。
當然,值得替換它的場景也不少。要傳大檔案,去找二進位格式;要寫人類友善的設定檔,去找 TOML;要效能,去找 Protobuf。但這些替代品都有一個共同點:它們總得在某個時刻承認「還是得跟 JSON 互通」,至少得提供轉換工具。這就是格式生態系的殘酷之處——你的競爭對手不只是功能,而是所有人的習慣。
順帶一提,json.org 那個沒有 CSS 的網站,二十年來就擺著一張圖和幾段話,再也沒多寫過什麼。某種意義上,這比任何宣傳都誠實:一個格式最好的一張名片,就是它根本不需要解釋自己。
