JSON 物件格式:
資料的通用語言
JSON 是前後端對話、API 回傳、設定檔、AI 回應的標準語言。 學完 JS 還沒學 JSON,等於學完中文還不會寫信。這章把 JSON 從語法到實戰、 從 schema 驗證到讓 LLM 乖乖回傳結構化資料,一次教完。
- 看得懂、寫得對任何 JSON(含巢狀、陣列、所有合法格式)
- 會用 JS / TS / Python 解析跟序列化
- 會寫 JSON Schema、用 Zod / Pydantic 驗證
- 會讓 OpenAI / Claude 強制回傳結構化 JSON
- 看得懂 REST API 的請求 / 回應格式
- 避得開 JSON 五大地雷(日期、Number 精度、null、循環引用、編碼)
JSON 是什麼,為什麼是事實標準
JSON = JavaScript Object Notation。1999 年從 JS 物件的字面量演化而來,現在是所有現代 API 預設的資料交換格式。
它打敗了誰:
- XML(2000 年代主流,太囉嗦)
- CSV(無法表達巢狀結構)
- YAML(人類好讀但機器解析慢、易出錯)
- protobuf(更快但二進位、不能眼睛看)
JSON 贏在:人讀得懂、機器解析快、所有語言都支援、夠簡單。
JSON 之於資料,像 USB Type-C 之於充電 — 不是最強,但哪都能用。學會它你跟任何系統溝通都不卡。
完整語法:6 種值、2 個容器
JSON 只認 6 種值:
{
"string": "用雙引號包起來",
"number": 42,
"boolean": true,
"null": null,
"array": [1, 2, 3],
"object": { "nested": "也行" }
}
嚴格規則(很多人踩雷)
- 字串只能用雙引號,單引號違法
- key 一定要加雙引號(跟 JS object 不一樣!)
- 最後一個元素不能有逗號(trailing comma 違法)
- 不能有註解(沒有
//也沒有/* */) - 不支援
undefined、NaN、Infinity
新手最常見錯誤:寫成 JS object 那樣 key 沒引號 → 整份 JSON 解析失敗。嚴格遵守雙引號規則,IDE 都會幫你檢查。
實戰例:一筆訂單
{
"order_id": "ORD-20260510-001",
"customer": {
"name": "招財",
"email": "hamster@snowrealm.tw"
},
"items": [
{ "sku": "GLA-001", "qty": 2, "price": 1800 },
{ "sku": "GLA-007", "qty": 1, "price": 3200 }
],
"paid": true,
"note": null
}
JS / TS / Python 的解析與序列化
JavaScript / TypeScript
// 字串 → 物件(解析)
const obj = JSON.parse(jsonString);
// 物件 → 字串(序列化)
const jsonString = JSON.stringify(obj);
// 美化輸出(縮排 2 格)
const pretty = JSON.stringify(obj, null, 2);
// 過濾欄位(只留 name 跟 email)
const filtered = JSON.stringify(obj, ["name", "email"]);
// 自訂轉換(每個值都過一遍)
const str = JSON.stringify(obj, (key, value) => {
if (key === "password") return undefined; // 移除
return value;
});
Python
import json
# 字串 → dict
data = json.loads(json_string)
# dict → 字串
json_string = json.dumps(data)
# 美化輸出 + 中文不轉 \uXXXX
pretty = json.dumps(data, indent=2, ensure_ascii=False)
# 讀檔 / 寫檔
with open("data.json") as f:
data = json.load(f)
with open("out.json", "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
Python json.dumps 預設會把中文變 \u4e2d\u6587,存檔給人類看一定要加 ensure_ascii=False。這個雷踩過的人都記得。
JSON Schema:給資料定規矩
JSON 自由 = 兩面刃。前端傳「年齡是字串」、後端期待「年齡是數字」就會壞。Schema 是寫給機器看的合約:「資料長這樣才合法」。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "user", "guest"]
}
}
}
JSON Schema 是業界標準。OpenAI、Anthropic 的 tool use、所有現代 API 文件(OpenAPI / Swagger)背後都是它。
Zod / Pydantic:型別驗證雙劍
JSON Schema 太囉嗦。實戰用程式碼定義 schema,自動產生型別 + 驗證 + 編輯器補全。
TypeScript:Zod
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1).max(50),
age: z.number().int().min(0).max(150),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"])
});
// 自動推導型別
type User = z.infer<typeof UserSchema>;
// 驗證(用在 API route)
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return Response.json({ error: result.error.flatten() }, { status: 400 });
}
const user: User = result.data; // 安全使用
Python:Pydantic
from pydantic import BaseModel, EmailStr, Field
from typing import Literal
class User(BaseModel):
name: str = Field(min_length=1, max_length=50)
age: int = Field(ge=0, le=150)
email: EmailStr
role: Literal["admin", "user", "guest"]
# 驗證
try:
user = User(**request_data)
except ValidationError as e:
return {"error": e.errors()}, 400
Zod 跟 Pydantic 還能反向產生 JSON Schema(zodToJsonSchema / Model.model_json_schema()),直接餵給 OpenAI / Claude 的 tool use,一份 schema 三邊用。
巢狀結構與資料正規化
真實資料常常巢狀。一個論壇貼文:
{
"id": "post-001",
"title": "AI 島開課",
"author": {
"id": "user-42",
"name": "Luffysky",
"avatar": "https://..."
},
"comments": [
{
"id": "c1",
"author": { "id": "user-42", "name": "Luffysky", "avatar": "https://..." },
"text": "歡迎留言"
}
]
}
看出問題嗎?author 重複了。同一個人改頭像要改幾十處。
正規化(normalize)
{
"posts": {
"post-001": { "title": "AI 島開課", "author_id": "user-42", "comment_ids": ["c1"] }
},
"comments": {
"c1": { "author_id": "user-42", "text": "歡迎留言" }
},
"users": {
"user-42": { "name": "Luffysky", "avatar": "https://..." }
}
}
後端 / Redux / Zustand 大型狀態幾乎都用這格式。資料庫思維帶進前端。
讓 LLM 強制回傳結構化 JSON
2026 年最值錢技能之一:把 AI 的「自由文字」鎖成「結構化資料」,這樣可以直接塞進 DB、串到下個流程。
OpenAI:response_format
const resp = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "分析這條評論:'真的超好吃,會再來!'" }],
response_format: {
type: "json_schema",
json_schema: {
name: "sentiment",
schema: {
type: "object",
properties: {
sentiment: { type: "string", enum: ["positive", "negative", "neutral"] },
score: { type: "number", minimum: 0, maximum: 1 },
keywords: { type: "array", items: { type: "string" } }
},
required: ["sentiment", "score", "keywords"]
}
}
}
});
const result = JSON.parse(resp.choices[0].message.content);
// { sentiment: "positive", score: 0.92, keywords: ["好吃", "會再來"] }
Anthropic Claude:tool use
const msg = await anthropic.messages.create({
model: "claude-opus-4-7",
max_tokens: 1024,
tools: [{
name: "submit_analysis",
description: "提交情緒分析結果",
input_schema: {
type: "object",
properties: {
sentiment: { type: "string", enum: ["positive", "negative", "neutral"] },
score: { type: "number" },
keywords: { type: "array", items: { type: "string" } }
},
required: ["sentiment", "score", "keywords"]
}
}],
tool_choice: { type: "tool", name: "submit_analysis" },
messages: [{ role: "user", content: "分析:'真的超好吃,會再來!'" }]
});
const result = msg.content.find(b => b.type === "tool_use").input;
用 Zod 寫 schema → 轉成 JSON Schema → 餵給 LLM → LLM 回傳 → 用同一個 Zod parse 一遍。編譯期型別 + 執行期驗證 + AI 結構化輸出三位一體。
JSON 在 API 設計的最佳實踐
RESTful API 統一回應格式
// 成功
{
"ok": true,
"data": { "id": "123", "name": "招財" },
"meta": { "page": 1, "total": 42 }
}
// 失敗
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "email 格式錯誤",
"fields": { "email": "必須是有效信箱" }
}
}
前端只要看 ok 就知道走哪條分支。錯誤 code 給機器、message 給人類。
欄位命名規範
- snake_case(後端傳統,多語言友善):
user_id - camelCase(JS 慣例):
userId - 挑一個全專案統一。混用是地獄。
分頁
{
"data": [/* ... */],
"pagination": {
"page": 2,
"per_page": 20,
"total": 142,
"total_pages": 8,
"next_cursor": "eyJpZCI6MTQwfQ=="
}
}
進階格式:JSONL、JSONPath、JMESPath
JSONL(JSON Lines)
每行一個 JSON 物件。串流處理、log 檔、AI 訓練資料的事實標準。
{"id":1,"text":"first"}
{"id":2,"text":"second"}
{"id":3,"text":"third"}
好處:可以邊讀邊處理(不用整檔載入),失敗一行不影響其他行。OpenAI fine-tune 資料就用這格式。
JSONPath:用路徑取值
// 對複雜物件做查詢
$.store.book[*].author // 所有書的作者
$.store.book[?(@.price < 10)] // 價格 < 10 的書
$..price // 任何深度的 price 欄位
JMESPath(AWS 出品,比 JSONPath 強)
// 抽欄位 + 重新組合
items[*].{name: name, total: price * qty}
items[?qty > `5`].name
sort_by(items, &price)
處理 AWS / API 回傳的大型 JSON 時超好用。aws-cli --query 就用這個。
五大地雷:踩過才會記得
地雷 1:日期沒有原生型別
JSON 裡的「日期」其實都是字串。約定俗成用 ISO 8601:
{ "created_at": "2026-05-10T11:30:00Z" }
不要用 "2026/05/10 11:30" 這種,跨時區會死。
地雷 2:Number 精度
JSON 的 number 走 IEEE 754 雙精度浮點,最大安全整數 2^53 - 1 = 9007199254740991。
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 } ← 精度遺失!
解法:大數字(雪花 ID、加密貨幣金額)一律存字串。
地雷 3:null vs undefined vs 不存在
// 三種「沒值」的差異
{ "a": null } // 明確說「沒有」
{ "a": undefined } // 違法!JSON 不認 undefined
{} // 連這個 key 都沒有
// JSON.stringify 會直接砍掉 undefined
JSON.stringify({ a: 1, b: undefined });
// '{"a":1}' ← b 不見了
地雷 4:循環引用
const obj = { name: "A" };
obj.self = obj; // 自己指自己
JSON.stringify(obj);
// TypeError: Converting circular structure to JSON
實戰常出現在 React component 把 DOM event 丟進 state 序列化。序列化前先抽純資料。
地雷 5:編碼
JSON 規範要求 UTF-8。Python json.dumps 預設 ASCII escape,中文變 \uXXXX。ensure_ascii=False 解決。
永遠不要用 eval() 解析 JSON — 駭客可以塞惡意 code。只用 JSON.parse()。這條救過無數網站。
🔨 動手練習:AI 客服訂單系統的 JSON Schema
設計一個完整的「AI 客服訂單系統」資料流:
- 用 Zod(或 Pydantic)寫
OrderRequestSchema:包含客戶資訊、商品陣列、付款方式 - 寫
AIResponseSchema:強制 LLM 回傳{ intent, action, reply, confidence } - 寫個
POST /api/customer-service收訂單問題,用 Claude API + tool use 強制結構化回應 - 失敗時回統一錯誤格式:
{ ok: false, error: { code, message, fields } } - 把所有 schema 用
zodToJsonSchema匯出,存進docs/api-schemas.json - 進階:把 LLM 回傳結果再用同一個 Zod schema 跑一次
parse,雙保險
常見卡關 FAQ
Q1. JSON 跟 JS object 到底差在哪?
JSON 是字串格式(給機器交換用),JS object 是記憶體裡的資料結構。JSON 規則嚴格(key 必加雙引號、不准 trailing comma、不准註解、不准 undefined),JS object 寬鬆。需要存檔 / 傳網路時轉 JSON 字串,要操作時轉回 object。
Q2. JSON 跟 YAML / TOML 怎麼選?
給機器看選 JSON(API、資料交換)。給人類設定選 YAML 或 TOML(Docker compose、CI 設定)。原因:YAML/TOML 支援註解,JSON 不支援。但 YAML 縮排敏感容易出錯,新專案越來越多選 TOML。
Q3. 為什麼後端傳給我的 JSON 我 fetch 完還要 .json()?
因為 HTTP response body 預設是字串流,不會自動解析。.json() 是 JSON.parse(await response.text()) 的捷徑。用 axios 的話它幫你做了所以不用再 parse。
Q4. 我可以信任 LLM 一定回傳合法 JSON 嗎?
不行。即使開了 response_format / tool use,偶爾還是會壞(網路斷、token 上限截斷)。永遠:(1) 用 try/catch 包 parse (2) parse 完再用 schema 驗一次 (3) 失敗時設計 retry 機制。
Q5. 大檔案 JSON(幾百 MB)怎麼處理?
不要整檔 JSON.parse,會爆記憶體。改用串流解析器:Node 用 stream-json、Python 用 ijson。或改成 JSONL 格式逐行處理。AI 訓練資料動輒幾 GB 都這樣處理。