JSON 工具箱

團隊協作常見問題:契約、配置與夾具

結論:命名風格、時間格式、空值語義、列舉形態、配置格式、夾具組織與合併衝突,十二條團隊裡真正會吵起來的取捨。

進階約 12 分鐘發佈於 2026-10-30更新於 2026-10-30
團隊協作常見問題封面

結論:一次性把這幾條寫進團隊約定——返回體用 {"data":…,"meta":…} 加 HTTP 狀態碼、命名全站統一同一種風格、時間用帶時區的 RFC 3339 字串、列舉用字串;之後所有新介面都按同一份契約「只加不改不刪」地演進。

這些問題沒有唯一正確答案,但必須有唯一團隊答案——否則每次新介面都要重新吵一遍。

一、介面契約

介面返回的 JSON 該用什麼結構?

三種常見形態,各有代價:

形態 例子 優點 缺點
裸資料 [{...}, {...}] 簡單 無法擴充套件元資訊
固定包裝 {"code":0,"message":"ok","data":{...}} 統一錯誤處理 業務碼與 HTTP 狀態重複
資源包裝 {"data":{...},"meta":{...},"links":{...}} 可擴充套件、語義清晰 層級多一層

對內的業務 API 推薦「資源包裝 + HTTP 狀態碼錶達結果」:

{
  "data": [{ "id": "1", "name": "示例" }],
  "meta": { "total": 128, "page": 2, "pageSize": 20 }
}

錯誤時用 HTTP 狀態碼 + 結構化錯誤體,不要再套一層 code:

{
  "error": {
    "type": "validation_failed",
    "message": "請求引數校驗失敗",
    "details": [
      { "path": "/email", "reason": "格式不正確" }
    ]
  }
}

關鍵點:HTTP 狀態碼已經表達了成功或失敗,業務碼是冗餘的。兩套體系並存時, 「201 但 code 是 500」這種不一致幾乎必然出現。

裸陣列要避免。頂層是陣列時無法附加分頁、總數、遊標,將來想加就得改所有呼叫方。

欄位命名用 snake_case 還是 camelCase?

選一個,全站統一,不要在同一份 JSON 裡混用。

風格 常見生態 例子
camelCase JavaScript / TypeScript / Java userName, createdAt
snake_case Python / Ruby / Go(部分) / 資料庫列名 user_name, created_at
kebab-case 罕見,鍵名裡帶連字元訪問不便 user-name

判斷依據:

  1. 誰消費這份 JSON:前端為主選 camelCase,資料管道與 Python 服務為主選 snake_case;
  2. 是否直接對映資料庫列:直接透傳表結構時用 snake_case 能省一層轉換;
  3. 存量系統延續:已有介面是什麼就用什麼,一致性比正確性重要。

真要轉換,放在邊界做(閘道器或前端請求層),不要在業務程式碼裡散落 toCamelCase 呼叫。

時間欄位用什麼格式?

用 RFC 3339 / ISO 8601 字串,帶時區偏移:

{
  "createdAt": "2026-10-30T11:37:57+08:00",
  "updatedAt": "2026-10-30T03:37:57Z"
}

要點:

  • 帶時區:不帶時區的 2026-10-30 11:37:57 在跨時區場景下必然出問題;
  • 統一 Z 或帶偏移:兩者都對,但一個介面裡別混用;
  • 不用時間戳數字:秒還是毫秒、是否含閏秒,每個團隊都要重新解釋一遍。需要緊湊時用毫秒整數, 但要在文件裡寫死單位。

欄位名建議用 createdAt / updatedAt / deletedAt 這類 At 字尾, 一眼能看出是時間點而不是時長。時長用 timeoutMs、ttlSeconds,單位寫進名字。

空值用 null 還是省略欄位?

取決於「這個欄位有沒有意義」:

語義 寫法 說明
欄位存在但暫無值 "name": null 明確表達「查過了,就是空」
欄位與該資源無關 省略 例如普通使用者不該有 vipExpireAt
部分更新(PATCH) 省略 = 不改 這時 null 才表示「清空」

第三種最容易被忽略。在 PATCH 請求裡:

{ "nickname": null }

意思是「把暱稱清空」,而請求體裡不出現 nickname 表示「不要動暱稱」。 如果兩者都當「清空」處理,使用者改個郵箱就會把暱稱清掉。

再強調一次:不要用 undefined。它不是合法 JSON 值,JSON.stringify 會直接丟掉這個鍵, 行為與「省略」相同,但程式碼裡看起來像「顯式傳了空」。

布林欄位怎麼命名?

用 is / has / can / should 字首,避免用否定式。

{
  "isActive": true,
  "hasPassword": false,
  "canEdit": true
}

三條經驗:

  1. 不要否定式:isDisabled: false 需要讀者做一次取反,isEnabled: true 更直接。 雙重否定(isNotDisabled)是災難;
  2. 不要用數字表示布林:"enabled": 1 會讓每個消費方都寫一次 === 1;
  3. 不要用字串:"enable": "true" 在 JavaScript 裡是 truthy, if (data.enable) 對 "false" 也成立,這類 bug 極難排查。

列舉欄位用數字還是字串?

用字串,除非有明確的儲存或頻寬壓力。

{ "status": "active" }

對比 { "status": 1 },字串的優勢:

  • 日誌、抓包、資料庫裡直接可讀,不用查對映表;
  • 新增列舉值不影響已有值的含義(數字列舉中間插入一個值就會錯位);
  • 前端不需要同步維護一份 1 → 'active' 的對映。

代價是多幾個位元組,壓縮後幾乎可忽略。

配套做法:字串列舉值用小寫 + 下劃線(pending_review),並在 Schema 裡用 enum 限定, 讓非法值在校驗階段就被攔住。

二、配置與夾具

配置用 JSON 還是 YAML / TOML?

按用途選,不要按喜好選:

格式 適合 不適合
JSON 程式讀寫、需要嚴格結構、要過 Schema 校驗 人工頻繁編輯(不能寫註釋)
YAML 多層巢狀的部署配置(K8s) 手寫容易縮排出錯;yes/no 會被轉成布林
TOML 扁平的依賴與工具配置(Cargo.toml、pyproject.toml) 深層巢狀可讀性差
JSONC 需要註釋的編輯器配置(tsconfig.json) 跨系統傳輸

團隊裡常見的錯誤是用 YAML 存業務配置——YAML 的隱式型別轉換(1.0 變數字、 NO 變布林、2026-10-30 變日期)會在不同解析器上產生不同結果。

如果配置要給程式做嚴格校驗,選 JSON + Schema,註釋用 _comment 欄位或獨立文件補充。

測試夾具怎麼組織?

按「一份基準 + 最小差異」組織,不要每個用例複製一整份巨大 JSON。

fixtures/
├─ users/
│  ├─ base.json              # 基準使用者物件
│  ├─ no-email.json          # 只有 email 缺失
│  └─ invalid-age.json       # 只有 age 越界
└─ fixtures.ts               # 載入與淺合併的工具函式
import baseUser from './users/base.json';

export function makeUser(overrides: Partial<User> = {}): User {
  return { ...baseUser, ...overrides };
}

這樣新增用例時只需要寫差異欄位,改基準欄位時所有用例自動跟上。

三條紀律:

  1. 夾具要能被 Schema 校驗:基準夾具本身就應該是合法資料;
  2. 不要從生產庫匯出真實資料:手機號、身份證、地址都是個人資訊;
  3. 夾具進版本庫,但要小:單檔案超過幾十 KB 就該考慮裁剪或生成。

JSON 檔案合併衝突怎麼減少?

JSON 天然容易衝突:格式化工具一次重排整個檔案,兩個人各改一行就會全檔案衝突。

四個手段:

  1. 統一格式化並加進 .editorconfig:縮排、換行、末尾換行一致,避免無關 diff;
  2. 排序鍵名:約定按字母序排列鍵,新增欄位不會和別人的插入位置打架;
  3. 拆分檔案:一個巨型 config.json 拆成按領域分的多個檔案,衝突面自然變小;
  4. 避免「一鍵重排」提交:格式化與業務改動分成兩個提交,review 時才看得清。

如果團隊用鎖檔案(pnpm-lock.yaml、package-lock.json),衝突時不要手工合併, 重新生成即可:

git checkout --theirs pnpm-lock.yaml
pnpm install

怎麼做版本相容?

原則是「只加不改不刪」。

變更 相容性 做法
新增可選欄位 相容 直接加,消費方忽略未知欄位
新增必填欄位 不相容 升版本,或給預設值
刪除欄位 不相容 先標記廢棄,觀察呼叫方後再刪
改欄位型別 不相容 新增欄位名,雙寫過渡
改列舉值集合 視情況 新增值相容,刪值不相容

配套機制:

  • 消費方用「寬鬆解析」:忽略未知欄位,欄位缺失時給預設值;
  • 服務端保留舊欄位一段時間,並在響應裡返回 Deprecation 頭提示;
  • Schema 裡對新增欄位不設 required,對廢棄欄位標 deprecated: true。

怎麼在 CI 裡校驗契約?

把 Schema 當成程式碼的一部分,提交時校驗。

# 校驗示例資料是否符合 Schema(用 ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false

CI 裡跑三件事:

  1. Schema 自身的合法性:$schema 宣告是否有效、$ref 能否解析;
  2. 夾具是否符合 Schema:保證測試資料不會漂移;
  3. 契約是否向後相容:對比上一個版本,檢查是否刪了欄位、改了型別。

第三項需要工具支援(如 openapi-diff、json-schema-diff), 沒有的話至少用指令碼對比 required 陣列與 properties 鍵集合的變化。

三、速查

問題 建議
返回結構 {"data":..., "meta":...},錯誤用 HTTP 狀態碼
命名風格 camelCase(前端為主)或 snake_case(資料為主),全站統一
時間格式 RFC 3339 字串,帶時區,欄位名以 At 結尾
空值 無值用 null,無關欄位省略,PATCH 裡省略表示不改
布林命名 is/has/can 字首,不用否定式,不用 0/1
列舉 字串,小寫下劃線,Schema 里加 enum
配置格式 程式校驗用 JSON,人工編輯用 TOML 或 JSONC
夾具 一份基準 + 最小差異覆蓋,能被 Schema 校驗
合併衝突 統一格式化、排序鍵名、拆分檔案
版本相容 只加不改不刪,廢棄欄位先標記再刪