JSONを受け取れただけでは入力契約にならない
JSONはAPI、設定ファイル、CAD属性などで広く使われます。しかし、JSONとして構文解析できることと、業務データとして正しいことは別です。
たとえば部品データのmass_kgが文字列になっている、必須のpart_idがない、未知のフィールド名が混ざっている、日時がタイムゾーン情報なしで送られる、といった問題はJSONパーサーだけでは検出できません。受信側ごとにif文を増やすと、APIとバッチで判定がずれ、エラー形式もばらばらになります。
JSON Schemaは、構造、型、必須フィールド、値の範囲などを機械可読なスキーマとして表す仕様です。この記事ではDraft 2020-12を使い、Python検証、失敗例、バージョン管理、DB・CADへの再利用までを組み立てます。
instanceとschemaを分けて考える
検証されるJSON値をinstance、そのルールを記述するJSON Schemaをschemaと呼びます。部品データのinstanceは次のように表せます。
{
"schema_version": "1.0",
"part_id": "PART-0042",
"revision": "B",
"mass_kg": 2.75,
"released_at": "2026-10-21T03:00:00Z",
"active": true,
"material": {
"material_id": "MAT-01",
"name": "Aluminum alloy"
}
}
最上位はobjectです。part_idなどがキー、その右側が値です。materialの値は別のobjectであり、内部にキーと値を持ちます。複数部品をまとめる場合は、これらのobjectをarrayの要素として並べます。
このinstanceに対するschemaを次のように定義します。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:example:schemas:part:1.0",
"type": "object",
"properties": {
"schema_version": {
"const": "1.0"
},
"part_id": {
"type": "string",
"pattern": "^PART-[0-9]{4}$"
},
"revision": {
"type": "string",
"minLength": 1
},
"mass_kg": {
"type": "number",
"exclusiveMinimum": 0
},
"released_at": {
"type": ["string", "null"],
"format": "date-time"
},
"active": {
"type": "boolean"
},
"material": {
"$ref": "#/$defs/material"
}
},
"required": [
"schema_version",
"part_id",
"revision",
"mass_kg",
"active",
"material"
],
"additionalProperties": false,
"$defs": {
"material": {
"type": "object",
"properties": {
"material_id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
}
},
"required": ["material_id"],
"additionalProperties": false
}
}
}
$schemaはdialectを示します。検証ライブラリの既定値へ依存せず明記します。$idはschemaを識別するURIです。このURNは説明用であり、実運用では組織が管理するURIとバージョン規則を使います。
type・properties・requiredは役割が違う
type: "object"はinstance全体がobjectであることを要求します。これを書かずにpropertiesだけを置くと、object以外へobject用キーワードが適用されず、意図しない値が検証を通る可能性があります。
propertiesは、キーごとに値のschemaを割り当てます。ただし、そこへ列挙しただけではフィールドは必須になりません。存在を要求するフィールドはrequiredへ明記します。
また、requiredが保証するのはキーの存在です。空文字にはminLength、正の値にはexclusiveMinimumを追加します。
| 確認したい内容 | 主なキーワード | 注意点 |
|---|---|---|
| JSON値の種類 | type | object、array、string、numberなどを明示する |
| フィールドのschema | properties | 列挙しただけでは必須にならない |
| フィールドの存在 | required | 空文字やnullまでは拒否しない |
| 文字列の長さ | minLength、maxLength | バイト数ではなく文字列の長さを扱う |
| 数値の範囲 | minimum、exclusiveMinimum | 単位や小数精度は別途契約する |
| 選択肢 | enum、const | 受理する値を明示する |
| 未知フィールド | additionalProperties | 既定では追加フィールドを許可する |
未指定・null・空文字を混同しない
次の三つは異なる状態です。
- キー自体が存在しない
- キーは存在し、値が
null - キーは存在し、値が空文字
""
released_atをrequiredへ含めていないため、未指定を許可しています。さらにtypeを["string", "null"]としているため、明示的なnullも許可しています。空文字はstringなので型だけなら通りますが、format検証を有効にすればdate-timeとして拒否できます。
未指定を「変更しない」、nullを「値を消す」と解釈するPATCH処理では、この差が特に重要です。schemaだけで更新動作は決まらないため、API契約に操作の意味も記述します。なお、前掲のschemaは部品データ全体を対象としたものであり、そのまま部分更新用のschemaになるわけではありません。
IDは数値に見えても計算対象とは限りません。"0012"と"12"を区別するならstringにします。金額、質量、寸法のように十進精度が重要な値は、JSON Schemaのnumberだけで保存精度まで決まりません。PythonのDecimalやDBの精度・scaleを別の変換契約として設計します。
additionalPropertiesで未知フィールドを扱う
additionalProperties: falseを設定すると、propertiesなどで定義していないキーを拒否できます。material_idの打ち間違いでmaterialIdが届いた場合に、値を無視して処理を続ける事故を防げます。
厳格な拒否はschemaの進化へ影響します。送信側が任意フィールドを追加すると、古い受信側は未知フィールドとして拒否します。次のいずれかを契約として選びます。
- 未知フィールドを拒否し、schemaのバージョンを明示的に切り替える
- 未知フィールドを受理して無視するtolerant readerを採用する
- エンドポイントやメディアタイプでmajor versionを分離する
また、allOfや$refでschemaを合成する場合、additionalPropertiesが認識するプロパティの範囲に注意が必要です。Draft 2020-12のunevaluatedPropertiesは、適用済みのsubschemaを考慮して未評価プロパティを制約できますが、合成構造と検証器の対応をテストしてから採用します。
formatは自動的に強制されるとは限らない
format: "date-time"を書けば、すべての検証器が不正な日時を拒否してくれるとは限りません。Draft 2020-12ではformatのannotationとassertionが分離されています。Pythonのjsonschemaでも、format checkerを渡さなければformatは検証されません。
日時、UUID、emailを制約に使う場合は、検証器の設定と追加依存パッケージを確認します。日時ではUTCオフセット、精度、許容期間も別途決めます。date-timeを満たすことだけでは、UTCのZ表記に統一されていることや、業務上妥当な期間内であることまでは保証できません。
patternは自動的に文字列全体へ固定されないため、先頭と末尾を指定するには^と$を付けます。末尾改行などの境界条件も、採用する検証器でテストしてください。複雑な正規表現を外部から受け取らず、schemaも信頼境界の内側で管理します。
$defsと$refで定義を再利用する
同じ材料objectを部品、図面、BOMで繰り返し記述すると、必須フィールドやpatternの変更漏れが起きます。$defsに再利用するsubschemaを置き、$refで参照すれば、同じドキュメント内で定義を共有できます。
外部schemaでは$idと参照URIを安定させ、schema一式をバージョン付きで配布します。実行中に任意URLから取得する構成は、通信障害、書き換え、SSRF、再現性低下につながるため、事前登録したregistryやbundleから解決します。
Pythonでschemaと入力を検証する
Pythonではjsonschemaパッケージを利用できます。date-timeなどのformat検証に必要な追加依存パッケージも含める例は次のとおりです。
python -m pip install "jsonschema[format]"
schemaをpart.schema.json、入力をpart.jsonへ保存します。次のコードをvalidate_part.pyとして同じフォルダーに保存すると、schema自身を確認したうえで、入力エラーをまとめて取得できます。
import json
from pathlib import Path
from jsonschema import Draft202012Validator, FormatChecker
def main() -> int:
schema = json.loads(
Path("part.schema.json").read_text(encoding="utf-8")
)
instance = json.loads(
Path("part.json").read_text(encoding="utf-8")
)
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(
schema,
format_checker=FormatChecker(),
)
# パスには文字列のキーと整数の添字が含まれる。
# 直接比較せず、JSON文字列にして表示順を安定させる。
errors = sorted(
validator.iter_errors(instance),
key=lambda error: (
json.dumps(list(error.absolute_path), ensure_ascii=False),
json.dumps(list(error.absolute_schema_path), ensure_ascii=False),
error.message,
),
)
if errors:
report = [
{
"path": list(error.absolute_path),
"schema_path": list(error.absolute_schema_path),
"keyword": error.validator,
"message": error.message,
}
for error in errors
]
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1
print(
json.dumps(
{"valid": True, "errors": []},
ensure_ascii=False,
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
実行コマンドは次のとおりです。ファイルを保存したフォルダーで実行してください。
python validate_part.py
検証に成功すると"valid": trueを出力し、終了コード0で終了します。入力がschemaに違反する場合はエラー配列を出力し、終了コード1で終了します。この例では、ファイル読み込み、JSON構文解析、schema自体の不備は例外として処理を停止します。本番の処理では、これらを入力の検証エラーと分けて扱ってください。
check_schemaは、入力を検証する前にschema自体がDraft 2020-12として妥当か確認します。iter_errorsを使うと、最初の一件で停止せず複数の問題を返せます。pathをarrayとして出力すれば、入れ子のobjectやarrayのどこで失敗したかを機械処理できます。
ただし、必須フィールドの欠落や未知フィールドのエラーでは、pathが対象フィールドそのものではなく、その親objectを示す場合があります。フィールド単位の画面表示へ変換する際は、キーワードごとの扱いも設計してください。
本番APIではライブラリのmessageをそのまま公開せず、invalid_typeやrequired_field_missingなど安定した公開エラーコードへ変換します。詳細message、schema path、受信値は内部ログへ分離し、秘密情報や個人情報を記録しすぎないようにします。
正常系と異常系を固定テストにする
schemaはドキュメントであると同時に実行可能な契約です。少なくとも次のケースを自動テストします。
- 正常な最小instanceが成功する
- 必須フィールドの欠落を拒否する
- 数値を表す文字列をnumberとして受理しない
- IDの先頭ゼロをstringのまま保持する
- null、未指定、空文字を意図どおり区別する
- 未知フィールドを許可または拒否する方針が一定である
- date-timeのformat検証が実際に有効である
- 入れ子のobjectとarrayのエラーパスを取得できる
- schemaのバージョンが一致しない入力を拒否する
- サイズ、深さ、配列要素数の上限を超える入力を入口で拒否する
前掲のschemaで、mass_kgを"2.75"へ変更すると型違反になります。0へ変更するとexclusiveMinimum違反、part_idを削除するとrequired違反です。released_atへ空文字を指定した場合は、format checkerが有効なら拒否されます。
リクエストのバイト数や入れ子の深さは、JSON構文解析より前、またはパーサーの設定で制限する必要があります。配列要素数はmaxItems、文字列長はmaxLengthなどでも制約できますが、前掲のschemaにはそれらの上限を設定していません。実際の入力契約に合わせて追加してください。
JSON Schemaは、material_idが指す材料の存在や業務上の一意性までは保証しません。DBの外部キーやlookup処理で確認します。BOMの循環、権限、状態遷移も別の層で検証します。
冪等性・バージョン・再実行は別に設計する
同じinstanceがschemaを通ることと、同じリクエストを二回処理してよいことは別です。連携処理には、安定した業務ID、一意制約、取込ID、ハッシュ、トランザクションを組み合わせます。
たとえば部品の最新状態を一件で管理するなら、part_idをDBの一意キーにし、同じデータの再送で同じ状態へUPSERTする設計が考えられます。改訂履歴を保持する場合は、part_idとrevisionの組み合わせなど、保存単位に合うキーを選びます。
古いrevisionで新しいレコードを上書きしないよう、送信元の更新番号や更新時刻も比較します。revisionの文字列を単純に辞書順で比較せず、改訂の順序規則を明示してください。schemaのバージョンと部品データの改訂は、それぞれ別に管理します。
schemaファイル自体はGitなどで履歴管理し、schemaのバージョン、適用開始日、互換性、移行手順を残します。
任意フィールドの追加も、未知フィールドを拒否する受信側には影響します。型変更、必須化、enum値削除、意味変更は破壊的になりやすいため、送信側と受信側の組み合わせを契約テストで確認します。
API・DB・CADへ再利用する設計
JSON Schemaを入力境界だけで終わらせず、共通のフィールド辞書として管理すると再利用しやすくなります。
typeをPython変換、APIフィールド、DB型の設計へ対応付けるrequiredとnullの許可を、APIの必須入力やDB制約の設計へ対応付けるenumを状態値や選択肢マスターと対応付ける$defsを部品、材料、図面、版などの共通構造へ分けるdescriptionや単位を表すフィールドをドキュメント生成へ再利用する- schemaのバージョンとデータのバージョンを監査記録へ残す
自動変換は完全ではありません。numberからDBの桁数やscaleは決まらず、formatの検証範囲も実装で異なります。また、requiredはキーの存在を要求するだけなので、DBのNOT NULLへ機械的に置き換えることはできません。nullを許す型かどうか、未指定時の既定値があるかも合わせて判断します。
CAD属性では単位、座標系、版、参照関係が必要です。生成できる部分と、人が業務上の意味を決める部分を分離します。
外部公開用schemaと内部schemaを分け、秘密情報を公開定義へ混ぜません。schema検証と認証・認可も独立して判定します。
まとめ
JSON Schemaは、JSONを「読めるデータ」から「検証できる入力契約」へ変える仕組みです。実務では次の点が重要です。
$schemaでdialectを固定するtype、properties、requiredの役割を分ける- 未指定、null、空文字を別の状態として設計する
additionalPropertiesの厳格さとschemaの進化を両立させる- format検証を検証器側で明示的に有効化する
$defsと$refで定義を再利用する- schema自身と正常系・異常系を自動テストする
- 参照整合性、冪等性、認可を別の層で守る
- バージョン付きschemaをAPI、DB、CAD連携で共有する
一つのobjectについて、instance、schema、異常系テスト、エラーレスポンスを同じリポジトリで管理すると、システム間の意味のずれを減らせます。
