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 求全。