REST API 的 JSON 最佳實務
命名、日期、ID、錯誤封包、分頁、版本控制——那些經得起時間考驗的 JSON 設計決策,與不行的那些。
REST API 在 JSON 上積累了數十年的慣例。遵循慣例可以無痛接上各種 客戶端函式庫、代碼生成器與開發者既有的心智模型;偏離慣例會帶來 永無止盡的支援工單。本文是有立場的劇本:經得起時間的選擇,以及 每項決定背後的理由。
一致的命名
選一種命名風格、貫徹到底。兩個合理選項:
camelCase——JavaScript 原生,給 web 與行動端使用最自然,是新 API 的主流。snake_case——Python、Ruby 原生,常見於老牌 API(Stripe、GitHub)與資料工程脈絡。
kebab-case 與 PascalCase 偶爾出現,但會在用 . 存取成員的語言
造成摩擦,避免。
規則是 一致,不是哪一種。混用——userId 旁邊放 created_at——
是最糟的選擇;如果內部使用另一種,就在邊界轉換。
回應形狀
「列出資源」endpoint 三種合理形狀:
裸陣列——GET /users 回傳 [{...}, {...}]。最簡單。無法加 meta
(分頁、總數)而不破壞客戶端。只在永不分頁的 endpoint 使用。
封套物件——GET /users 回傳 { "data": [...], "meta": {...} }。
日後可加 meta 而不破壞舊客戶端。新 API 的預設選擇。
{
"data": [
{ "id": "user_1", "name": "Ada" },
{ "id": "user_2", "name": "Alan" }
],
"meta": {
"page": 1,
"pageSize": 20,
"total": 142
}
}
JSON:API——{"data": [...], "included": [...], "links": {...}}。
重量級但有規範,免費處理關聯。當你的領域真的是圖狀且消費端能從共用
規格受益時最佳。
不要自創新的包裝——從這三者擇一。
錯誤:RFC 7807
錯誤請用 RFC 7807 Problem Details,
它是標準錯誤格式,Content-Type: application/problem+json。
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 402,
"detail": "Your account balance is $5; the transaction requires $10.",
"instance": "/accounts/acct_123/transactions/txn_456",
"balance": 5,
"required": 10
}
五個欄位:
type——識別錯誤類別的 URI(客戶端依此分派)。title——人類可讀的簡短摘要。status——HTTP 狀態碼(與回應一致)。detail——人類可讀的詳細說明。instance——特定發生點的 URI(對客服很有用)。
加上特定錯誤類別的 擴充欄位。驗證錯誤通常會帶 errors 陣列,
列出欄位路徑與訊息。
型別:數字、日期、金額、ID
四個最常踩雷的型別決定:
數字。JSON 數字是十進位文字,但 JavaScript 客戶端解析成 64-bit float。超過 ±2^53 就會失真。三類值必須以字串編碼:
- ID——
"user_123"或"123456789012345"——字串。即使內部是數字 ID,輸出也應字串化以避免失真並保留未來彈性。 - 金額——含小數位的字串(
"19.95"),或最小幣別單位的整數(1995表示 $19.95)。絕不要用浮點。 - 大型計數——字串。
日期。ISO 8601 字串,含時區。永遠。
{
"createdAt": "2026-05-13T10:30:00Z",
"birthday": "1985-03-12"
}
完整時間戳記以 UTC(Z 後綴);純日期不含時間。不要用秒數的
Unix 時間戳——它在精度上模糊、人類不可讀、客戶端必須知道哪些欄位
是日期。
布林。只有 true 與 false。不要用 "yes"/"no" 或 0/1。
Enum。字串、全小寫、kebab-case 或 snake-case 與全 API 一致。
"status": "in-progress",不要 "status": 2。
null、缺值、空字串
三種狀態有不同意義:
- 缺值(沒有
"middleName"鍵)——「我們沒這資料,或不適用」。 - null(
"middleName": null)——「我們問過,使用者沒填」。 - 空字串(
"middleName": "")——通常是 UI bug;使用者在必填欄沒輸入。在 API 端轉成null或直接拒絕。
請在「缺值」與「null」之間擇一作為「不存在」並保持一致。混用(有時
鍵不在、有時為 null)會導致無止盡的客戶端 bug。多數現代 API 永遠
輸出鍵、以 null 表示不存在——對型別生成器與
TypeScript 都更友善。
分頁
兩種模式:
- 頁碼——
?page=2&pageSize=20。容易理解、能曝露總數,但在資料新增/刪除時分頁會跳。 - Cursor——
?cursor=opaque-token。在插入與刪除下穩定;任何 append-heavy 資源(事件、日誌、訊息)的正確選擇。
不論哪種,請在回應中回傳 next cursor 或 next 連結,讓客戶端不必
自己算:
{
"data": [...],
"links": {
"next": "https://api.example.com/events?cursor=eyJ0IjoxNzMxNTAwMDAwfQ"
}
}
版本
影響最長期的決定,三種真實選項:
- URL——
/v1/users、/v2/users。最顯眼、最容易路由、鼓勵大型遷移。 - Header——
Accept: application/vnd.example.v2+json。URL 乾淨,但版本對使用者隱形。 - 連續式——不分版本,只能加新欄位與新 endpoint,不可移除或改名。當你能控制兩端時最佳。
連續式做得到就值得。否則 URL 版本最少意外。
比格式更重要的規則:加欄位不算 breaking change。客戶端應忽略未知 欄位。移除或更名是 breaking,需升版。
傳輸:Content-Type、壓縮、最小化
- Content-Type——回應用
application/json,錯誤用application/problem+json。2026 年已無需指定 charset——UTF-8 是規格預設。 - 壓縮——gzip 或 brotli。超過幾百位元組必開。
- 最小化——回應 yes、版控 fixture 與設定 no。請見 壓縮與美化 JSON。
Content-Length或Transfer-Encoding: chunked——擇一設定。大型回應改用串流。
以 JSON Schema 與 OpenAPI 文件化
可給工具吃的 JSON 合約勝過散文文件十倍。兩條可行路徑:
- 每個 endpoint 一份 JSON Schema——為每個請求/回應寫一份。同時用於執行期驗證(伺服端)與型別生成(客戶端)。見 JSON Schema 新手入門。
- OpenAPI——把 schema、URL、auth、範例打包成一份文件。每個 API client 生成器(
openapi-typescript、openapi-generator-cli)都吃。
內部服務雙端皆你掌控時,schema 驅動與 OpenAPI 都行。公開 API 請選 OpenAPI,那是生態圈預期的格式。
工具
- /zh-Hant/json/formatter——隨手美化或壓縮 payload。
- /zh-Hant/json/validator——以 JSON Schema 驗證 payload。
- /zh-Hant/json/types/typescript——從範本回應生成型別。
延伸閱讀
- JSON Schema 新手入門——寫下 API 強制的合約。
- 在 JavaScript 中安全解析 JSON——客戶端處理回應。
- 壓縮與美化 JSON——何時送哪種形式。