CI での JSON 検証:jsonlint、ajv、jq によるパイプライン
要点:構文検証はファイルがパースすることを証明し、スキーマ検証はフィールドが契約に従うことを証明します。CI では明確な終了コード、エラー位置、修復経路が重要です。

要点:CI では構文を jq empty、構造を ajv-cli(複数形式のチームは check-jsonschema)、スタイルを Prettier に任せ、層ごとに連結して最初の失敗で止めましょう。
ローカルエディタでパーサーを通ることは、そのファイルが CI に属することを意味しません。CI を通ることは、フィールドが契約に一致することを意味しません。
1. 構文検証とスキーマ検証を分ける
「JSON 検証」という言葉はあまりに広く使われ、そのためにチームは二つの異なる問題を一つのツールで解こうとします。
| 層 | 答える問い | 典型的なツール | 失敗が意味すること |
|---|---|---|---|
| 構文 | このファイルは正しい JSON か | jq empty、jsonlint、python -m json.tool |
括弧、カンマ、引用符、エンコーディングが壊れている |
| スキーマ | フィールド、型、必須プロパティが契約に一致するか | ajv-cli、check-jsonschema |
ファイルはパースするが業務構造が誤り |
| スタイル | インデントとキー順が一貫しているか | Prettier、jq -S |
整形が不統一で差分が騒がしい |
これらの層は互いを置き換えられません。構文を通ったファイルでもスキーマで失敗しえます。{"price": "12.5"} は正しい JSON ですが、スキーマが price を数値と要求するなら受け入れられません。逆に、スキーマがどれほど完全でも、閉じ括弧を欠くファイルは決してそれに適合しません。
ですからパイプラインは構文、構造、スタイルの順に実行し、層が失敗したらすぐ止めます。さもないとパースエラーがフィールドエラーを装います。
2. 構文層:jq empty は依存最小の選択
CI イメージに jq がすでにあれば、構文検証はほとんど無料です。
jq empty data.json
jq empty は何も出力せず、ファイルをパースするだけです。成功時は 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 は呼び出しごとに 1 ファイルにしてエラーメッセージが犯人を特定できるようにします。数百ファイルを単一プロセスに渡さないでください。エラーメッセージが上書きし合います。
jsonlint も一般的な選択で、通常は npx jsonlint -q data.json と呼び出し、-q はエラーだけを出力します。行と列番号を報告しますが、保守は jq より活発でなく、バージョンによって BOM とコメントの許容が異なります。より読みやすい構文チェッカーとして扱い、スキーマバリデータとして扱わないでください。
3. スキーマ層:ajv-cli validate の基本
Ajv は JavaScript エコシステムで最も広く使われるスキーマバリデータで、ajv-cli がコマンドラインツールとして公開します。
ajv validate -s schema.json -d data.json
-s はスキーマファイル、-d は検証するデータを指します。失敗時はプロセスが非ゼロで終了し、CI は即座に失敗します。実際のプロジェクトは通常二つのフラグを加えます。
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false は古いスキーマに役立ちます。strict モードは未知のキーワードと疑わしい required 宣言をエラーにするので、多くのレガシースキーマが失敗します。--errors=text は人間向けの出力を生み、既定の JSON 構造は機械向けです。結果を誰が消費するかで選びます。
スキーマのドラフトバージョンも一致しなければなりません。Ajv のメジャーバージョンは既定で異なるドラフトをサポートし、ajv-cli は --spec=draft7、--spec=draft2019、--spec=draft2020 で選択できます。スキーマが $schema: "https://json-schema.org/draft/2020-12/schema" を宣言しているのに CLI が draft-07 としてコンパイルすると、一部のキーワードが静かに無視されます。スキーマ作成とバージョン差の詳細は JSON Schema 入門を参照してください。
形式検証も頻繁な要件です。format キーワードは既定で注釈のみで、email、date-time、uri を実際に強制するには ajv-formats を読み込みます。
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
この手順は見落とされやすく、結果として email フィールドが 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 から一般的なスキーマを取得できますが、ローカルファイルも受け入れ、ネットワーク依存を避けられます。
言語をまたぐチームでは Python がすでに入っていることが多く、check-jsonschema の導入と更新は Node ツールチェーンの維持より軽いです。構文は jq、構造はこのツールに任せると、保守オーバーヘッドが非常に低い組み合わせになります。
4. 終了コードだけが信頼できる CI 信号
出力がどれほど洗練されていても、CI は終了コードしか読みません。よくある三つの罠:
| 症状 | 原因 | 修正 |
|---|---|---|
| エラーがあるのに CI が緑 | コマンドが ` | |
| エラーがないのに CI が赤 | diff が「差異あり」を失敗と扱う |
「差異」と「実行エラー」を区別する |
| エラー出力が途切れる | 並列の複数ファイル出力が上書きし合う | 直列実行するかファイルごとに出力 |
jq の終了コードの意味を覚えておきましょう。通常のパースは 0、-e 付きで式の結果が false か null なら 1、使用法エラーとファイル欠落は 2、スクリプトのコンパイルエラーは 3 です。スキーマツールは一般に成功 0、検証失敗 1、ツールや引数のエラー 2 です。ラッパーを書くとき、すべての非ゼロコードを「データが間違っている」に潰さないでください。環境障害が内容問題と誤診されます。
5. 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 も受け入れます。フックにリポジトリ全体を走査させないでください。コミットが目に見えて遅くなります。
6. GitHub Actions:コピーできるパイプライン
以下のワークフローは構文検証、スキーマ検証を行い、失敗時にレポートを保存します。
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 に足してバージョンを固定すべきです。ロックがなければ、既定のドラフトや strict モードを変える上流のリリースが一晩でパイプラインを赤にできます。
7. エラー位置:行番号からフィールドパスまで
バリデータの価値の半分は検出、もう半分は位置特定です。三種類の出力を読めるようにしましょう。
- 構文エラー:
jqは行と列を報告し、しばしば近くの断片も示します。まず括弧とカンマを直し、すぐにエンコーディングを疑わない。 - スキーマエラー: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 のようなマッピング行を足します。
additionalProperties: false の過用や、実際には重複する oneOf 分岐の記述など、よくあるスキーマの間違いは JSON Schema 生成のよくある落とし穴で扱っています。誤った検証規則は検証なしより危険です。偽の安全感を生むからです。
8. ツール選択表
| ニーズ | 推奨ツール | 理由 | 注意 |
|---|---|---|---|
| 構文のみ | jq empty |
依存ゼロ、高速、明確な終了コード | 一括実行時のファイル名の空白 |
| より親切な行/列エラー | jsonlint |
読みやすい出力 | 保守の活発さは限定的 |
| JSON Schema 検証 | ajv-cli |
成熟したエコシステム、構造化エラー | ドラフト版の一致と format 対応の読み込み |
| JSON と YAML の混在リポジトリ | check-jsonschema |
複数形式、pre-commit 親和 | スキーマの出所とネットワークアクセス |
| コミット前の迅速なフィードバック | pre-commit / lint-staged | ステージ済みファイルだけを処理 | フックは複数パスを受け入れる必要 |
| 整形の一貫性 | Prettier | 広く採用 | ファイルを書き直したくないときは --check |
最後の推奨:意図的に不正なレコードを一つ流して、パイプラインが実際に失敗することを確認しましょう。最大のリスクは複雑な設定ではなく、決して効かない設定です。「失敗できる」を合格基準にする方が、「通る」を基準にするより信頼できます。より広いコマンドラインツールチェーンはエディタ、コマンドライン、自動化に続きます。
FAQ
jq empty と jsonlint のどちらを使うべきですか?
CI では jq empty を優先してください。依存ゼロ、高速な起動、明確な終了コードの意味があります。より親切な行/列メッセージが必要なときやローカル調査では jsonlint を使います。どちらも構文検証だけで、フィールド構造はチェックしません。
ajv が未知のキーワードを報告するのはなぜですか?
まずスキーマの $schema ドラフトが CLI の使うバージョンと一致するか確認してください。「未知のキーワード」エラーの多くは、古いコンパイラが新ドラフトのキーワードに出会ったものです。バージョンが一致するなら綴り間違いを探します。--strict=false はレガシースキーマを動かし続けられますが、本当の問題を隠すために長期的に頼らないでください。
スキーマ検証は通るのに API が失敗するのはなぜですか?
スキーマは構造を検証し、業務意味論は検証しません。たとえば status が正当な enum の文字列でも、状態機械が paid から refunded への直接遷移を禁じているかもしれません。そのような規則は JSON Schema ではなく、コードやデータベース制約に属します。
失敗したフィールドを素早く特定するには?
ツール出力の instancePath か行番号を読みます。Ajv の instancePath はデータを直接指す JSON Pointer で、check-jsonschema はファイルと行を報告します。パスを記録して業務キーにマッピングすれば、問題の特定は通常数分です。
pre-commit で完全な検証を実行すべきですか?
いいえ。pre-commit はステージ済みファイルだけを処理し、コミットを速く保つべきです。全体走査は CI か定期ジョブに属します。分担は単純です。ローカルは速度、CI は完全性を優先します。