JSON 工具箱

介面回傳的 JSON 該用什麼結構

結論:三種常見結構各有代價:裸回傳最簡潔但擴充時要破壞相容;固定信封安全但冗餘;JSON:API 規範完整但重。中小型專案最實用的是「HTTP 狀態碼表結果 + 固定信封包業務資料」。

進階約 4 分鐘發佈於 2026-10-30更新於 2026-10-30
三種介面回應結構及其取捨

結論:用 4xx/5xx 狀態碼表達成敗,用 { "data": ..., "meta": ... } 承載業務資料,錯誤時換成 error 信封;內部專案做到這一步就夠,只有對外開放平台才需要再引入 JSON:API 或 OpenAPI。

先排除最差的一種:永遠回 200,把成敗寫在 code 欄位裡。

三種主流結構

A. 裸回傳

{ "id": 1, "name": "demo", "price": 99 }

最簡潔,HTTP 語意完整(404 就是 404)。問題在於想加元資訊(分頁、警告、除錯資訊)時沒有位置,只能臨時加欄位,破壞原有契約。

B. 固定信封

{
  "data": { "id": 1, "name": "demo" },
  "meta": { "requestId": "abc", "took": 12 }
}

業務資料與元資訊分離,加欄位不破壞 data 的形狀。代價是多一層巢狀。

C. 全資訊信封(不推薦)

{ "code": 0, "message": "success", "data": {...} }

問題不在結構本身,而在配套做法:HTTP 狀態碼永遠 200,錯誤碼自訂。後果是 CDN、閘道、熔斷、重試、監控全部看不懂真實成敗。

推薦組合

用 HTTP 狀態碼表達結果,用信封承載業務資料。

成功:

{
  "data": { "id": 1, "name": "demo" },
  "meta": { "requestId": "req_8f2a", "took": 12 }
}

失敗:

{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "訂單不存在或已被刪除",
    "details": [{ "field": "id", "issue": "not_found" }]
  },
  "meta": { "requestId": "req_8f2a" }
}

狀態碼用 4xx/5xx,code 用機器可讀的穩定識別(不是給人看的中文),message 給人看,details 放欄位級錯誤供表單回填。

幾條硬規則

1. 列表一定要有分頁元資訊。 沒有 total 或游標,前端就沒法做分頁控件。

2. 空列表回 [],不要回 null。 前端 .map 直接崩是常見事故。

3. 時間統一 ISO 8601 帶時區。 見 時間欄位用什麼格式。

4. 欄位命名全站統一。 snake_case 還是 camelCase 選一個。跨語言時轉換放在邊界層做一次。

5. 金額用整數最小單位或字串。 99.99 在浮點裡不精確,9999(分)或 "99.99" 都行,但要在文件裡寫死。

6. 別把 null 和「欄位不存在」混用。 兩者語意不同,見 空值用 null 還是省略欄位。

什麼時候上規範

  • 對外開放平台、多方對接:考慮 JSON:API 或 OpenAPI 描述 + 統一信封
  • 內部前後端一體:固定信封足夠,別過度設計
  • 需要強型別契約:OpenAPI/JSON Schema 先行,結構從 Schema 產生

規範的價值在於「不用每次討論」,成本在於「每個新人都要學」。五個人的專案上 JSON:API,多數時候是負收益。

常見問題

裸回傳和固定信封該怎麼選?

要分頁、警告、requestId 這類元資訊時用固定信封,讓 meta 與 data 分開成長。完全不需要元資訊、且能接受以後加欄位會破壞契約時,才用裸回傳。

為什麼不該永遠回 200?

CDN、閘道、熔斷、重試和監控都只看 HTTP 狀態碼來決定是否快取、重試或告警。狀態碼永遠是 200 時,它們全都把失敗當成成功,每個告警還要自己解析業務碼。

列表介面是否必須帶分頁元資訊?

是。沒有 total 或游標,前端就做不出分頁控件;同時空列表要回 [] 而不是 null,否則前端的 .map 會直接報錯。

延伸閱讀