「JSON Schemaで入力契約を固定する設計|型・必須・定義再利用・Python検証」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

JSON Schemaで入力契約を固定する設計|型・必須・定義再利用・Python検証

JSON Schema Draft 2020-12でJSON入力を検証する設計を解説。型、必須項目、NULL、未知field、format、定義再利用、Python実装、version管理まで実務例で整理します。

DOC NO.EKP-00-1144
DATE
REVB
READ約 16 分

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値の種類typeobject、array、string、numberなどを明示する
フィールドのschemaproperties列挙しただけでは必須にならない
フィールドの存在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、異常系テスト、エラーレスポンスを同じリポジトリで管理すると、システム間の意味のずれを減らせます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次