JSON 校驗進 CI:jsonlint、ajv 與 jq 的流水線
結論:語法校驗只能保證檔案「能解析」,Schema 校驗才能保證欄位「符合約定」。把兩者放進 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
它的幾個實用點:
- 原生支援 YAML,適合 Kubernetes、CI 配置這類「JSON 結構、YAML 寫法」的檔案;
- 報錯會帶上檔案路徑、行號與欄位路徑,定位成本低;
- 提供 pre-commit 鉤子,接入成本低;
- 支援從 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 並鎖定版本。版本不鎖,某天上游升級預設草案或嚴格模式,流水線會突然變紅。
七、報錯定位:從行號到欄位路徑
校驗工具的價值一半在「能發現」,另一半在「能定位」。三類輸出要會讀:
- 語法錯誤:
jq給出行列號,通常還會帶上附近片段。先修括號與逗號,不要先懷疑編碼; - Schema 錯誤:Ajv 的
instancePath指向資料位置(如/items/0/price),schemaPath指向規則位置(如#/properties/items/items/properties/price/type),keyword說明違反了哪條規則; - 格式錯誤:
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 求全。