JSON 工具箱

JSON 校驗進 CI:jsonlint、ajv 與 jq 的流水線

結論:語法校驗只能保證檔案「能解析」,Schema 校驗才能保證欄位「符合約定」。把兩者放進 CI,關鍵不是選哪個工具,而是讓退出碼、報錯定位與修復路徑都清楚。

進階約 10 分鐘發佈於 2026-10-31更新於 2026-10-31
JSON 校驗進 CI 封面

結論:CI 裡語法校驗交給 jq empty,結構校驗交給 ajv-cli(跨格式團隊用 check-jsonschema),風格交給 Prettier;按層次串聯,前一層失敗就停。

能在本地編輯器裡通過解析,不代表能進 CI;能讓 CI 通過,也不代表欄位符合約定。

一、先分清語法校驗與 Schema 校驗

「JSON 校驗」這個詞被用得太泛,導致團隊經常拿一個工具去解決兩類問題:

層次 回答的問題 典型工具 失敗時說明什麼
語法校驗 這份檔案是不是合法 JSON? jq empty、jsonlint、python -m json.tool 括號、逗號、引號、編碼出了問題
Schema 校驗 欄位、型別、必填項是否符合約定? ajv-cli、check-jsonschema 檔案能解析,但業務結構不對
風格校驗 縮排、鍵序是否統一? Prettier、jq -S 格式不統一,diff 雜訊大

三者不能互相替代。語法校驗通過但 Schema 校驗失敗的例子非常常見:{"price": "12.5"} 是合法 JSON,但如果 Schema 要求 price 是數字,它就不合格。反過來,Schema 寫得再全,檔案本身少了右括號,編譯 Schema 也沒有意義。

因此流水線應該是「先語法、後結構、最後風格」,前一層失敗時立即停止,避免把解析錯誤偽裝成欄位錯誤。

二、語法層:jq empty 是最小依賴的選擇

如果 CI 映像裡已經有 jq,語法校驗幾乎零成本:

jq empty data.json

jq empty 不輸出任何內容,只解析檔案;成功時退出碼為 0,解析失敗時返回非 0 並給出行列資訊。它比引入一整套 Node 依賴更輕,適合當作第一道閘門。

批次校驗倉庫裡的 JSON 檔案時,注意排除 node_modules、建置產物與第三方快取:

find . -name '*.json' \
  -not -path './node_modules/*' \
  -not -path './dist/*' \
  -print0 | xargs -0 -n1 jq empty

-print0 與 xargs -0 是為了相容檔案名稱裡的空格;-n1 保證一次只傳一個檔案,報錯時能準確知道是哪一個。不要用 xargs 一次性把幾百個檔案塞給一個行程,錯誤資訊會互相覆蓋。

jsonlint 是另一個常見選擇,命令形如 npx jsonlint -q data.json,-q 表示只輸出錯誤。它的優點是報錯帶行列號,缺點是生態維護不如 jq 活躍,且不同版本對 BOM、註解的容忍度不一致。把它當作「可讀性更好的語法檢查」可以,不要指望它承擔 Schema 校驗。

三、Schema 層:ajv-cli validate 的基本用法

JavaScript 生態裡最常用的 Schema 校驗器是 Ajv,ajv-cli 把它包裝成命令列工具:

ajv validate -s schema.json -d data.json

-s 指定 Schema 檔案,-d 指定待校驗資料;校驗失敗時行程退出碼為非 0,CI 會直接失敗。實際專案裡通常還會加兩個參數:

npx ajv-cli validate \
  -s schemas/order.json \
  -d 'fixtures/orders/*.json' \
  --strict=false \
  --errors=text

--strict=false 用於相容一些歷史 Schema:Ajv 在嚴格模式下會把未知關鍵字、可疑的 required 寫法當成錯誤,老 Schema 往往過不了。--errors=text 讓輸出更適合人讀,預設的 JSON 結構更適合機器消費,二者按場景選。

Schema 草案版本也要對齊。Ajv 主版本預設支援的草案不同,ajv-cli 可以透過 --spec=draft7、--spec=draft2019、--spec=draft2020 選擇;如果 Schema 裡寫了 $schema: "https://json-schema.org/draft/2020-12/schema",而 CLI 仍按 draft-07 編譯,有些關鍵字會被靜默忽略。Schema 的寫法與版本差異可以繼續看 JSON Schema 入門。

另一個高頻需求是格式校驗。format 關鍵字預設只做註解,要讓 email、date-time、uri 真正生效,需要載入 ajv-formats:

npx ajv-cli validate -c ajv-formats -s schema.json -d data.json

這一步經常被漏掉,結果是「信箱欄位填了 not-an-email 也能過」。上線前用一條故意錯誤的資料驗證規則確實生效,比讀十遍文件更可靠。

check-jsonschema:多格式與更友善的報錯

如果團隊不只處理 JSON,還要校驗 YAML、TOML 或 GitHub Actions 配置,check-jsonschema 會更順手:

check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml

它的幾個實用點:

  1. 原生支援 YAML,適合 Kubernetes、CI 配置這類「JSON 結構、YAML 寫法」的檔案;
  2. 報錯會帶上檔案路徑、行號與欄位路徑,定位成本低;
  3. 提供 pre-commit 鉤子,接入成本低;
  4. 支援從 SchemaStore 拉取常見 Schema,但也允許本地檔案,避免網路依賴。

跨語言團隊裡,Python 往往已經存在,check-jsonschema 的安裝與升級比 Node 工具鏈更輕。把「語法」交給 jq、把「結構」交給它,是一條維護成本很低的組合。

四、退出碼是流水線唯一可信的訊號

工具輸出再漂亮,CI 只認退出碼。常見的坑有三個:

現象 原因 處理
有錯誤但 CI 綠 命令後面接了 `
沒錯誤但 CI 紅 diff 把「有差異」也當失敗 區分「差異」與「執行錯誤」
報錯資訊被截斷 多檔案並行輸出互相覆蓋 串行執行或按檔案單獨輸出

jq 的退出碼語義要特別記:正常解析返回 0;-e 模式下表示式結果為 false 或 null 返回 1;用法錯誤、檔案不存在等返回 2;指令碼編譯錯誤返回 3。Schema 工具通常用 0 表示通過、1 表示校驗失敗、2 表示工具或參數錯誤。寫包裝指令碼時,不要把所有非 0 都籠統歸為「資料有問題」,否則環境故障會被誤判成內容問題。

五、pre-commit:把問題攔在提交前

pre-commit 的價值不是「多一道檢查」,而是讓開發者在一分鐘內拿到回饋,而不是等 CI 跑完十分鐘。一個最小配置如下:

repos:
  - repo: https://github.com/python-jsonschema/check-jsonschema
    rev: 0.33.0
    hooks:
      - id: check-jsonschema
        files: ^configs/.*\.json$
        args:
          - --schemafile
          - schemas/config.json
  - repo: local
    hooks:
      - id: jq-syntax
        name: jq syntax check
        entry: jq empty
        language: system
        files: ^configs/.*\.json$

兩個細節:

  • files 只匹配需要校驗的目錄,不要對 node_modules 或生成檔案跑鉤子;
  • 本地鉤子依賴開發者機器已安裝 jq,團隊需要統一安裝說明,或者改用容器化鉤子。

如果專案已經有 Husky 與 lint-staged,也可以直接組合:

{
  "lint-staged": {
    "configs/*.json": ["jq empty", "prettier --check"]
  }
}

注意 lint-staged 傳入的是暫存檔案列表,命令要能接受多個路徑;jq empty 支援多檔案,prettier --check 也支援。不要讓鉤子處理整個倉庫,提交會明顯變慢。

六、GitHub Actions:一條可複製的流水線

下面這條流水線做了三件事:語法校驗、Schema 校驗、失敗時保留報告:

name: json-validate

on:
  push:
    paths:
      - 'schemas/**'
      - 'fixtures/**'
  pull_request:

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Syntax check
        run: |
          find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
      - name: Schema check
        run: |
          npx --yes ajv-cli validate \
            -s schemas/order.json \
            -d 'fixtures/orders/*.json' \
            --strict=false \
            --errors=text

paths 過濾能避免文件改動也觸發校驗;npx --yes 適合沒有鎖定依賴的倉庫,生產專案更建議把 ajv-cli 寫進 devDependencies 並鎖定版本。版本不鎖,某天上游升級預設草案或嚴格模式,流水線會突然變紅。

七、報錯定位:從行號到欄位路徑

校驗工具的價值一半在「能發現」,另一半在「能定位」。三類輸出要會讀:

  1. 語法錯誤:jq 給出行列號,通常還會帶上附近片段。先修括號與逗號,不要先懷疑編碼;
  2. Schema 錯誤:Ajv 的 instancePath 指向資料位置(如 /items/0/price),schemaPath 指向規則位置(如 #/properties/items/items/properties/price/type),keyword 說明違反了哪條規則;
  3. 格式錯誤:format 校驗失敗時,資訊通常只寫「must match format "email"」,要結合 instancePath 才能知道是哪個欄位。

把 instancePath 直接貼進排查記錄,比貼一整段 JSON 有效得多。對於陣列,路徑裡的數字下標還能告訴你「第幾條記錄」;如果業務主鍵更直觀,可以在報錯後補一行映射,例如 items[0].id = ORD-042。

Schema 本身的常見坑,例如過度使用 additionalProperties: false、把 oneOf 寫成互斥但實際重疊,可以看 Schema 生成的常見陷阱。校驗規則寫錯,比不校驗更危險,因為它會給人虛假的安全感。

八、工具選擇表

需求 推薦工具 理由 注意
只查語法 jq empty 零依賴、快、退出碼清晰 批次時注意檔案名稱空格
要更友善的行列報錯 jsonlint 輸出直觀 維護活躍度一般
JSON Schema 校驗 ajv-cli 生態成熟、錯誤結構化 對齊草案版本與 format 外掛
JSON + YAML 混合倉庫 check-jsonschema 多格式、pre-commit 友善 注意 Schema 來源與網路
提交前快速回饋 pre-commit / lint-staged 只處理暫存檔案 鉤子要能接收多路徑
統一格式 Prettier 生態通用 只檢查不修改時用 --check

最後一條建議:先用一條故意寫錯的資料驗證流水線真的會紅。校驗工具最怕的不是配置複雜,而是配置了卻沒生效。把「能失敗」當成驗收標準,比把「能通過」當成驗收標準更可靠。完整的命令列工具鏈可以繼續看 編輯器、命令列與自動化工具鏈。

常見問題

jq empty 和 jsonlint 應該選哪個?

CI 裡優先 jq empty:零依賴、啟動快、退出碼語義清楚。需要更友善的行列提示或在本地手工排查時,再用 jsonlint。兩者都只做語法校驗,不負責欄位結構。

ajv 報錯說 unknown keyword 怎麼辦?

先確認 Schema 的 $schema 草案版本與 CLI 使用的版本一致。很多「未知關鍵字」其實是新草案的關鍵字被舊版本編譯器遇到。確認版本後仍報錯,再檢查是否拼錯了關鍵字;必要時用 --strict=false 相容歷史 Schema,但不要長期依賴它掩蓋問題。

Schema 校驗通過,為什麼介面還是出錯?

Schema 只能驗證結構,驗證不了業務語義。比如 status 是字串且列舉合法,但狀態機不允許從 paid 直接跳到 refunded;這類規則要用程式碼或資料庫約束,不要硬塞進 JSON Schema。

校驗失敗時怎麼快速定位到具體欄位?

看工具輸出的 instancePath 或行號。Ajv 的 instancePath 是 JSON Pointer 形式,直接指向資料位置;check-jsonschema 會給檔案與行號。把路徑記錄下來,再對照業務主鍵,通常幾分鐘內能定位。

要不要在 pre-commit 裡跑全量校驗?

不要。pre-commit 只處理暫存檔案,才能保持提交速度。全量掃描放到 CI 或定時任務裡,兩者分工明確:本地求快,CI 求全。