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