CHAPTER 05 / JSON

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、循環引用、編碼)
LESSON 5.1

JSON 是什麼,為什麼是事實標準

JSON = JavaScript Object Notation。1999 年從 JS 物件的字面量演化而來,現在是所有現代 API 預設的資料交換格式

它打敗了誰:

JSON 贏在:人讀得懂、機器解析快、所有語言都支援、夠簡單

JSON 之於資料,像 USB Type-C 之於充電 — 不是最強,但哪都能用。學會它你跟任何系統溝通都不卡。

LESSON 5.2

完整語法:6 種值、2 個容器

JSON 只認 6 種值:

{
  "string": "用雙引號包起來",
  "number": 42,
  "boolean": true,
  "null": null,
  "array": [1, 2, 3],
  "object": { "nested": "也行" }
}

嚴格規則(很多人踩雷)

⚠ 踩雷警告

新手最常見錯誤:寫成 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
}
LESSON 5.3

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。這個雷踩過的人都記得。

LESSON 5.4

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)背後都是它。

LESSON 5.5

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 SchemazodToJsonSchema / Model.model_json_schema()),直接餵給 OpenAI / Claude 的 tool use,一份 schema 三邊用。

LESSON 5.6

巢狀結構與資料正規化

真實資料常常巢狀。一個論壇貼文:

{
  "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 大型狀態幾乎都用這格式。資料庫思維帶進前端

LESSON 5.7

讓 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 結構化輸出三位一體。

LESSON 5.8

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 給人類。

欄位命名規範

分頁

{
  "data": [/* ... */],
  "pagination": {
    "page": 2,
    "per_page": 20,
    "total": 142,
    "total_pages": 8,
    "next_cursor": "eyJpZCI6MTQwfQ=="
  }
}
LESSON 5.9

進階格式: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 就用這個。

LESSON 5.10

五大地雷:踩過才會記得

地雷 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,中文變 \uXXXXensure_ascii=False 解決。

🚨 資安警報

永遠不要用 eval() 解析 JSON — 駭客可以塞惡意 code。只用 JSON.parse()。這條救過無數網站。

🔨 動手練習:AI 客服訂單系統的 JSON Schema

設計一個完整的「AI 客服訂單系統」資料流:

  1. 用 Zod(或 Pydantic)寫 OrderRequestSchema:包含客戶資訊、商品陣列、付款方式
  2. AIResponseSchema:強制 LLM 回傳 { intent, action, reply, confidence }
  3. 寫個 POST /api/customer-service 收訂單問題,用 Claude API + tool use 強制結構化回應
  4. 失敗時回統一錯誤格式:{ ok: false, error: { code, message, fields } }
  5. 把所有 schema 用 zodToJsonSchema 匯出,存進 docs/api-schemas.json
  6. 進階:把 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 都這樣處理。

← 上一章
04 TS