JSON Schemaとは?JSONの構造を検証する書き方とDraft 2020-12の実装を実装者目線で解説【2026年版】
JSON Schemaとは、JSONデータがどういう形をしているべきかをJSON自身で書き表すための語彙です。この項目は文字列で必須、この配列の要素は0以上の整数、といった条件を宣言として並べ、受け取ったデータがそれを満たすかを機械に判定させます。検証をif文で書き下すのではなく、条件をデータとして外へ置ける点がこの仕様の性格を決めています。扱う範囲は、基本の記法、条件分岐と部品化、draft-07から2020-12へ移すときに壊れる箇所、AjvとPythonでの実装、そしてコード側の型定義とどちらを正に置くかという判断です。入力検証そのものの目的と種類はバリデーションの設計と実装パターンにまとめてあります。
まとめ|JSON Schemaで買えるのは境界の検証と仕様の単一化
この仕様が売っているのは、検証条件をコードの外へ出せることと、その1枚を言語をまたいで共有できることです。同じスキーマをNode.jsのAPIサーバとPythonのバッチとフロントの入力画面が読めば、判定基準は1か所にまとまる。仕様書と検証コードが別々に育って食い違う事故が構造的に減らせます。
対価は2つ。表現力に限界があり、他のレコードと突き合わせて分かる条件は書けません。もう1つは記法の癖で、additionalPropertiesがallOfと組むと効かない、formatは既定で検証されない、requiredは空文字列を弾かない。判断としては、複数の作り手が同じJSONを読み書きする境界にだけ置き、単一言語で完結する内部処理には持ち込みません。
JSON Schemaとは|JSONの構造をJSON自身で宣言して検証する語彙
誤解されやすい仕様なので、何であって何でないかを先に押さえます。
スキーマの正体|型と必須と制約を宣言するだけのJSONオブジェクト
JSON Schemaで書いたスキーマは、それ自体が普通のJSONオブジェクトです。決められた名前のキー(キーワード)に値を書くと、それが検証条件として解釈されます。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" }
},
"required": ["id", "email"]
}
読み方はそのままで、オブジェクトであること、idは1以上の整数、emailは文字列、この2つは必須。スキーマがJSONである意味は運用側に出ます。APIで配れる、差分がテキストとして読める。専用言語で書く方式との違いはここに集約されます。
バリデーションとの関係|手続きで書く検証と宣言で書く検証の差
検証処理は普通、手続きとして書きます。値を取り出して型を調べ、条件を満たさなければエラーを積む。柔軟ですが、条件がコードに埋まるため他の言語からは読めません。
JSON Schemaは同じことを宣言で書き、判定ロジックは検証ライブラリが持ちます。得られるのは移植性と機械可読な仕様書、失うのは表現力で、開始日と終了日の前後関係のような項目間の条件は一部しか書けない。宣言の層と手続きの層を重ねるのが定石で、検証全体の組み立ては入力検証の種類と設計の側にまとめてあります。
どこで効かせるか|APIの入口と設定ファイルと保存前の3か所
置き場所は用途で決まり、価値が出るのは次の3か所です。
- APIの入口:リクエストボディを処理の前段で弾き、仕様書の記述をそのまま検証へ回せる
- 設定ファイル:エディタが補完と警告を出すため、設定ミスが編集時点で分かる
- 保存前とデータ連携:取り込んだJSONを確かめ、壊れたデータの流入を止める
共通するのは、どれもシステムの境界だという点です。自分が書いたコードから自分が書いたコードへ渡すデータには要りません。生成AI連携でも同じ構図が出ており、モデルの出力形式をJSON Schemaで縛る機能が各社の実装に入っています。
基本の書き方|type・properties・requiredで形を宣言する手順
オブジェクト・文字列と数値・配列の順に記法を押さえます。
オブジェクトの検証|requiredとadditionalPropertiesの効き方
オブジェクトはpropertiesで各項目の条件を書き、requiredで必須項目名を配列で並べます。ここで最初の落とし穴。propertiesに書いた項目は必須ではなく、条件は「もしその項目があれば」という前提で効きます。
2つ目は余分な項目の扱いです。既定ではスキーマに書いていない項目が入っていても通り、弾きたければadditionalPropertiesにfalseを指定します。ただしこれはallOfで組み合わせた瞬間に効かなくなる。各サブスキーマは独立に評価され、additionalPropertiesは自分と同じ階層のpropertiesしか見ないためです。共通部分と個別部分をallOfで足すと個別部分の項目が余分と判定されるので、unevaluatedPropertiesへ置き換えます。
文字列と数値と配列|minLengthやprefixItemsで値域を絞る
文字列はminLength・maxLength・pattern(正規表現)・formatで絞ります。ここで3つ目の落とし穴。formatは仕様上、既定では検証しないアノテーション扱いで、emailと書いても検証ライブラリが有効にしていなければ何も起きません。
もう1つ、requiredは項目の存在だけを見ます。値が空文字列でも「ある」と判定されるため、画面入力の必須チェックにはminLengthを1に指定する条件を併記してください。数値はminimum・multipleOf、配列はitemsで全要素の条件、minItems・uniqueItemsで個数と重複を縛ります。要素ごとに型が違うタプル形式は、2020-12でprefixItemsへ分かれました。
主要キーワード一覧|型ごとに使える制約を表で引けるようにする
使用頻度の高いキーワードを型別に並べます。
| 対象 | キーワード | 意味 | 注意点 |
|---|---|---|---|
| 全般 | type | データ型の指定 | 配列で複数型も可 |
| 全般 | enum | 取りうる値の列挙 | 型を問わず比較する |
| オブジェクト | required | 必須の項目名 | 空文字列は弾かない |
| オブジェクト | additionalProperties | 未定義項目の可否 | allOfとは併用不可 |
| 文字列 | pattern | 正規表現の一致 | 部分一致で判定する |
| 文字列 | format | 日付やメール等の書式 | 既定では検証しない |
| 数値 | minimum | 下限値 | 未満はexclusive指定 |
| 配列 | prefixItems | 先頭から順の型指定 | 2020-12で分離された |
組み合わせと条件分岐|allOfとoneOfとif-then-elseの使い分け
業務データは単純な型の羅列では表せません。種別によって必須項目が変わる条件の書き方を整理します。
allOfとanyOfとoneOf|どれか一つだけ通す条件の書き分け方
3つの違いは論理演算そのものです。allOfは全て満たすこと、anyOfは1つ以上、oneOfはちょうど1つだけ。共通項目の再利用ならallOf、複数形式の受け入れならanyOf、排他的な種別判定ならoneOfを使います。
問題を起こすのはoneOfです。ちょうど1つという条件は、サブスキーマが互いに排他でなければ成立しません。個人と法人の2形式を並べたつもりでも、共通項目しか書いていなければ両方に一致して失敗する。加えて、どれにも一致しなかったときは候補それぞれの不一致理由が全て返るためエラーが読みにくい。排他が種別コード1項目で決まるなら、次のif-then-elseのほうが素直です。
if-then-elseの分岐|種別ごとに必須項目を変える書き方
「種別が法人なら会社名を必須にする」という条件はif-then-elseで書きます。ifの条件が満たされたらthenを、満たされなければelseを適用する形です。
{
"type": "object",
"properties": {
"kind": { "enum": ["personal", "corporate"] },
"companyName": { "type": "string", "minLength": 1 }
},
"required": ["kind"],
"if": { "properties": { "kind": { "const": "corporate" } } },
"then": { "required": ["companyName"] }
}
注意点が1つ。ifの中でその項目をrequiredにしていないと、項目が存在しないデータではif条件が成立せずthenも適用されません。判定対象はifの中でも必須として書くのが安全です。3種類以上の分岐は入れ子になりますが、3段を超えるなら手続き側へ逃がしてください。
再利用と参照|$defsと$refでスキーマを部品として組み立てる
同じ定義の繰り返しを部品化するのが参照の仕組みです。
$defsと$refの基本|同じ定義を1か所へまとめて参照する
$defsは再利用する定義の置き場で、$refがそこを指します。値はJSONポインタで書き、同一ファイル内なら番号記号で始まる相対参照になります。
{
"$defs": {
"money": {
"type": "object",
"properties": {
"amount": { "type": "integer", "minimum": 0 },
"currency": { "enum": ["JPY", "USD"] }
},
"required": ["amount", "currency"]
}
},
"type": "object",
"properties": {
"subtotal": { "$ref": "#/$defs/money" },
"tax": { "$ref": "#/$defs/money" }
}
}
置き場の名前はドラフトで変わりました。draft-07まではdefinitionsという慣習的な名前で、2019-09から$defsが正式なキーワードになっています。古い記事を写しても参照先のパスが合えば動くため混在に気づきにくい。新規なら$defsで統一してください。
$idと$schemaの役割|ドラフト宣言と解決の基点を明示する
$schemaはそのスキーマがどのドラフトで書かれているかの宣言で、検証ライブラリはこれを見て解釈を切り替えます。省略するとライブラリの既定ドラフトで解釈される。Ajvの既定はdraft-07なので、2020-12の記法を宣言なしで渡すとprefixItemsが未知のキーワードとして無視されます。$idのほうはスキーマの識別子で、複数ファイルに分けて相互参照する構成では各ファイルへ振り、検証ライブラリへ事前登録するのが基本形です。参照先をネットワーク越しに取りに行かせる構成は実行時の障害点になるため、ビルド時に束ねるか事前登録へ寄せてください。
ドラフト版の違い|draft-07から2020-12へ移すときに壊れる箇所
json-schema.orgの仕様ページは、現行版が2020-12で前版が2019-09だと明記しています(2026年8月時点)。
2020-12の主な変更|prefixItemsと$dynamicRefへの置き換え
移行で壊れる筆頭は配列です。draft-07ではitemsに配列を渡すとタプル形式になり、余りはadditionalItemsで縛る仕様でした。2020-12ではタプル形式がprefixItemsへ分離され、itemsはそれより後ろの全要素を指します。古い書き方のまま$schemaだけ書き換えると配列が解釈されず、落ちるのではなく素通りするためテストがなければ気づけません。
他には再帰的な参照が$recursiveRefから$dynamicRefへ置き換わりました。日常的に書く範囲で影響が出るのは、配列とdefinitionsの名称の2つです。
ドラフト版を上げる判断|ツールの対応状況で決まる移行の現実的な順序
どの版で書くかは、仕様の新しさではなく周辺ツールの対応で決めます。判断材料は、検証ライブラリの対応、コード生成や文書生成ツールの対応、配る相手の環境の3つ。
2026年8月時点では、Python向けのjsonschemaが4.26.0でDraft 2020-12から3までを網羅しています。Ajvは8.20.0で2020-12に対応するものの、既定の読み込み先はdraft-07向けのままで別エントリが要る。この「対応はしているが既定ではない」状態がバグの温床です。新規なら2020-12で始め、既存がdraft-07なら外部要因が来たときにまとめて移すのが現実的な順序になります。
実装|Ajvとjsonschemaで検証を通しエラーを利用者へ返す
スキーマを渡してエラーを受け取るまでの流れを見ます。
Node.jsのAjv|コンパイル済み検証関数を使い回す実装の形
Node.jsで広く使われているのがAjvです。スキーマをJavaScriptの関数へコンパイルしてから使うため、同じスキーマを繰り返す用途では検証が高速に回ります。
const Ajv = require("ajv/dist/2020");
const addFormats = require("ajv-formats");
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(schema);
if (!validate(payload)) {
console.log(validate.errors);
}
押さえる点は3つ。コンパイルは起動時に1回だけ行い関数を使い回すこと。既定ではエラー1件で止まるため、画面全体の誤りを一度に返すなら allErrors を有効にすること。そしてformatには専用パッケージが要ることです。
Pythonのjsonschema|Draft202012Validatorで全件検出
Pythonでは jsonschema パッケージが標準的な選択肢です。単発ならvalidate関数を呼ぶだけですが、実務では検証器を作って全件のエラーを集める形が使いやすい。
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(payload), key=lambda e: e.json_path)
for e in errors:
print(e.json_path, e.message)
iter_errors は不一致を1件ずつ返すため、どの項目がどう違うかをまとめて集められます。json_path で誤りの位置が取れるので、画面の項目名へ対応付ければ表示に使える。版を固定したいなら、検証器クラスを明示するこの書き方が安全です。
エラーの返し方|検証結果をRFC 9457のレスポンスへ載せる
検証ライブラリが返すエラーは、そのまま利用者へ出す形にはなっていません。英語の技術メッセージで、項目の日本語名も入っていない。機械可読な位置情報と人間向けの文言は分けて載せます。
HTTP APIのエラー応答には標準の型があり、RFC 9457のProblem Detailsなら拡張メンバへ項目ごとの誤り一覧を載せられます。項目名と誤り種別のペアへ変換する層を挟めば、検証ライブラリを差し替えても応答形式は変わりません。
OpenAPIとの関係|3.1で完全互換になった範囲と3.0の乖離
この仕様に触れる経路として最も多いのがOpenAPIです。関係は版によって変わるため、誤解すると仕様書と検証がずれます。
OpenAPI 3.1との互換|スキーマオブジェクトが完全互換になる
OpenAPI 3.0のスキーマオブジェクトは、JSON Schemaの拡張サブセットという位置付けでした。使えないキーワードがあり、逆にnullableのような独自語彙もあった。この乖離のせいで仕様書のスキーマをそのまま検証に使えませんでした。
OpenAPI 3.1はDraft 2020-12との完全互換をうたっており、スキーマオブジェクトはJSON Schemaそのものになります。仕様書のスキーマをそのまま検証へ回せて、1枚から型定義と検証器と文書をまとめて生成できる。nullableは廃れ、型を配列で並べてnullを許す書き方へ移ります。仕様書側の書き方はOpenAPIの仕様書の書き方を参照してください。
型定義との使い分け|ZodとJSON Schemaのどちらを正とするか
どちらを単一の情報源に置くかで運用が変わります。
コード側を正とする|Zodから生成して型と検証と文書を一致させる
コード側を正に置く流派では、TypeScriptでスキーマを書いて型を推論し、必要になったときJSON Schemaへ変換して外へ出します。利点は開発体験で、エディタ上で型が効き、JSONの入れ子を組む必要がない。Zodの導入と記法はZodの使い方入門にまとめてあります。
制約は変換の非対称性です。TypeScript側の任意の関数による判定はJSON Schemaへ変換できず、結果が元より緩くなる。外へ配ったスキーマで通るデータが本体では弾かれる食い違いが起きます。ライブラリ間の乗り換えはStandard Schemaの取り組みが進んでいますが、これはTypeScript内部での互換であり、言語をまたぐ問題は解きません。
スキーマ側を正とする|複数の言語をまたぐときに崩れない置き方
JSON Schemaを正に置く流派では、スキーマファイルを一次資料として管理し、各言語の型はそこから生成します。開発体験は劣りますが、参加する言語が2つを超えた時点で優位が逆転します。
判断は言語の数で言い切れます。TypeScriptだけで完結し外部へ配る予定もないならコード側を正にする。フロントがTypeScript、バックエンドがPythonやGo、連携先の外部システムもいるならJSON Schemaを正にする。中間として、コード側で書きつつビルド時に生成したスキーマを外部向けの契約として扱う方式もありますが、生成物を手で編集しないという運用ルールを明文化しないと両方が正のまま食い違います。
採用する条件と見送る場面|JSON Schemaを書く価値が出る境界線
書く価値が出る条件を切り分けます。全ての入力に置く必要はありません。
採用してよい条件|複数の作り手が同じJSONを読み書きする場面
導入して効果が出るのは、次の条件に当てはまる場面です。
- 外部の組織や別チームがJSONを送ってくる(受け入れ条件を文書と検証で示せる)
- 複数の言語で同じデータを扱う(判定基準が1か所にまとまる)
- 設定ファイルを人が手で編集する(補完と警告が編集時に効く)
- 生成AIの出力を後続処理へ渡す(形式を縛って後段の破損を防ぐ)
共通するのは、条件を書いた人と読む人が別だという性質です。本質的な価値は検証の実行ではなく、条件が独立したデータとして存在することにあり、だから相手が増えるほど得をします。契約として成立させるにはAPI全体の設計と揃える必要があるため、システム間連携を前提に組むならAPI開発・システム連携で設計段階からの相談を受け付けています。
見送ってよい場面|単一の言語で完結する内部処理だけの入力検証
反対に、持ち込むと保守の手間だけが増える場面もあります。単一言語で完結する内部処理、自分が書いたコードへ渡すだけのデータ、条件が項目間の複雑な関係に依存するもの。この3つは手続きで書くほうが短く済みます。
判定の基準は、そのJSONが自分の管理外から来るかどうかです。来ないならスキーマは要りません。二重管理が実際に事故を起こしているという事実が先にあって、初めて投資が正当化されます。仕様記述の厳密さではXMLのXSDが先行しており、その系譜はSOAPとXSDの仕組みの側にまとまっています。
運用で決めること|スキーマの置き場所と版上げの手順を先に決める
採用を決めたら、書き始める前にファイルの置き場所と版上げの手順を決めてください。置き場所は実装リポジトリへ同居させるか独立させるかの二択で、連携先が社外なら独立、社内だけなら同居で足ります。
版上げで決めるべきは互換性の扱いです。項目の追加は既存データを壊しませんが、requiredへの追加とenumからの削除は壊す。この2つのときだけ版番号を上げて併存期間を置くルールを先に敷いてください。
よくある質問
導入で問い合わせの多い5点をまとめます。
JSON SchemaとJSONの違いは何ですか?
JSONはデータの書き方そのもので、JSON Schemaはそのデータがどういう形をしているべきかを書き表す語彙です。スキーマ自体もJSONで書かれるため、ファイルとしては両方ともJSONになる。JSONが個々のデータ、JSON Schemaがその仕様書兼検証条件という関係です。schema.orgの構造化データとも名前が似ていますが、あちらはWeb上の情報へ意味を付ける語彙で別のものです。
どのドラフトを選べばよいですか?
新規なら Draft 2020-12 を選んでください。json-schema.orgが現行版として示しているのがこの版で、Python向けのjsonschemaもAjvも対応しています。既存が draft-07 で動いているなら急いで移す必要はありません。移行の最大の変更は、itemsに配列を渡すタプル形式がprefixItemsへ分離された点です。古い記法のまま宣言だけ書き換えると検証が素通りするため、移行時はテストで確認してください。
additionalPropertiesがfalseなのに余分な項目が通るのはなぜですか?
allOfと組み合わせているのが原因です。additionalPropertiesは同じ階層のpropertiesしか参照しないため、allOfの各サブスキーマで定義した項目は知らない項目として扱われます。2019-09以降で使えるunevaluatedPropertiesはサブスキーマが評価した項目まで踏まえるので、この用途ではそちらへ置き換えてください。
formatを書いたのに検証されないのはなぜですか?
仕様上、formatは既定でアノテーション扱いだからです。検証するかどうかは実装に委ねられており、検証ライブラリ側で明示的に有効化するまで素通りします。Ajvではformat検証が別パッケージへ分離されているため、追加して登録する手順が必要です。確実に弾きたい場合はpatternで正規表現を書くか、format検証を有効にしたうえで動作をテストで確認してください。
項目どうしの関係はJSON Schemaで書けますか?
一部は書けますが限界があります。種別が法人なら会社名が必須といった条件は、if-then-elseやdependentRequiredで表現できる。一方で開始日が終了日より前という値どうしの比較や、データベースを参照する重複チェックは書けません。前者をJSON Schema、後者をアプリケーション側の検証で受け持つ二段構えにしてください。全てをスキーマへ寄せると入れ子が深くなり保守できなくなります。