こんにちは、エンジニアのタカです。 今回は、私が直近で開発業務で使用している JsonSchema(ジェイソン・スキーマ) の紹介と、Pythonの Pydantic(パイダンティック)モデル と組み合わせたバリデーションについて解説します。 JSONのメリットとのデメリット JSON (JavaScript Object Notation) は「キーと値のペア」というシンプルで直感的なデータ定義形式が特徴です。 比較的自由に値を定義でき、高い可読性を持つ上に、プログラミング言語に依存しないフォーマットであるため、現在ではデータ交換フォーマットの 事実上の標準 として、多くのシステムで利用されています。 一方で、このJSONの柔軟性は、時に以下のような課題を引き起こすことがあります。 構造の不透明性 : JSON自体には、どのようなデータが存在し得るかという「型」や「必須項目」を定義する仕組みがありません。別途ドキュメントを準備する必要があり、また、アプリケーション側で意図しないデータが紛れ込んだり、あるいは期待するデータが存在しないといった状況が発生しやすくなります。 データ品質の低下 : スキーマが存在しないことにより、データの中身に対する開発者間の認識にズレが生じやすくなります。これが原因でデータの品質が低下したり、予期せぬバグを引き起こしたりするリスクがあります。 バリデーションの複雑化 : 受信したJSONデータのバリデーションをアプリケーション側で手動で実装しようとすると、コードが複雑化し、結果として保守性の低下を招く恐れがあります。 私が現在開発に携わっているシステムでも、PostgreSQLのテーブルに jsonb 型のカラムを設け、JSON形式で可変長のデータを保存しています。 このデータはユーザーごとにJSONに保存するキーが異なる場合があり、一貫したデータ形式にならず、データのバリデーションに苦労していました。この問題を解消するために、 JsonSchema という定義を導入することにしました。 JsonSchemaとは JsonSchemaは、JSONデータの構造やルールを定義するスキーマ言語です。簡単に言うと、「 JSONデータがどんな形をしているべきか 」というルールを定めるもので、JSONの柔軟性ゆえに生じる前述の「構造の不透明性」や「データ品質の低下」といった課題を解消するために定義されました。 このJsonSchemaには、主に以下の3つの用途があります。 データのバリデーション : 入力データが期待する形式に合致しているかの検証 コード生成 : 定義されたスキーマから任意の言語向けのデータモデルクラスやバリデータコードの自動生成 ドキュメンテーション : データ構造の定義による開発者間の共通認識の形成 これらの用途について、実際のJsonSchemaのサンプルを交えて解説します。こちらのサンプルは、架空のユーザーデータを表したものです。 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/user.json", "title": "User", "description": "User profile data", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the user" }, "name": { "type": "string", "minLength": 1, "maxLength": 100, "description": "User Name" }, "email": { "type": "string", "format": "email", "description": "User email" }, "is_active": { "type": "boolean", "default": true, "description": "User active status. active is true" } }, "required": ["id", "name", "email"] } JsonSchemaは、規定のキーワードを用いて表現されます。以下に、代表的なものを記載します。 キーワード 役割 $schema スキーマが準拠するJsonSchemaのバージョンURI $id スキーマを一意に識別するためのURI title , description スキーマの名称と説明 type データの基本型( object , string , integer , boolean など) properties type が object の場合に、オブジェクトが持つプロパティとその定義 required オブジェクトのプロパティのうち、必須となるもののリスト 1. データのバリデーション JsonSchemaの最も基本的な用途がデータの バリデーション です。 properties キーワードの中に各プロパティごとのルールを定めることで、受け取ったJSONデータが期待通りの形式や値の範囲に適合しているかを自動的に検証できます。 前述のユーザーデータのサンプルでは、 id は uuid 形式の文字列、 name は1文字以上100文字以下の文字列、 email はメールアドレス形式の文字列、 is_active は真偽値でデフォルトは true 、といったルールになります。 また、 id 、 name 、 email は required キーワードによって必須項目と定義されます。 このような厳格なデータチェックをコードで手動実装すると複雑になりがちですが、JsonSchemaを使えば宣言的にルールを記述できるため、可読性と保守性を高められます。 なお、AIモデルが生成する出力形式を指定する structured output (構造化出力) というアプローチでもJsonSchemaが利用されており、AIモデルの出力をJsonSchemaで定義した形式に指定することで、AIの出力を他のシステムやアプリケーションが自動的に処理・解析しやすくなります。 2. コード生成 JsonSchemaは、データ構造の厳密な定義となるため、そこから アプリケーションコードを自動的に生成 する基盤にもなります。 フロントエンド、バックエンドを問わず、JsonSchemaから各種ツールを用いて以下のようなものを生成できます。これらは、開発プロセスの効率化や、手動実装でのミスを削減する効果があります。 生成物 例 説明 データモデルクラス PythonのPydanticモデル、 TypeScriptのインターフェースなど JsonSchemaで定義されたJSONの構造を、各プログラミング言語のオブジェクトとして扱うためのクラスやインターフェース バリデータコード 関数、クラス JsonSchemaに記述された minLength や format 、 pattern といった具体的な制約に基づき、入力データが正しい形式であるかを検証するためのコード(関数やクラス) APIドキュメント OpenAPI/Swaggerなど 人間が読みやすい形式のAPIリファレンスドキュメント(Swagger UIのような対話型ドキュメント)。JsonSchemaは、OpenAPI Specificationの基盤としても使用されている なお、本記事で後述する datamodel-code-generator はPydanticモデルを生成するツールです。スキーマが変更された際も、ツールを再実行すれば関連クラスを最新の状態に保つことができます。 3. ドキュメンテーション JsonSchemaそのものが、JSONデータの持つプロパティの種類、データの型、制約、そして説明を詳細に記述しているため、これ自体が質の高いドキュメントとして機能します。 さらに、JsonSchemaのエコシステムには、JsonSchemaをより視覚的で分かりやすいHTMLドキュメントなどに変換するツールも存在します。個別のツールについては、 公式サイトのToolページ に記載があるので参照ください。 ただし、ドキュメンテーションツールに限らず、JsonSchemaのツールに関しては、対応するJsonSchemaバージョンによって利用できるものとそうでないものがあります。利用の際には $schema キーワードで指定するバージョンに対応するものを選ぶ必要があります。 (補足) JsonSchemaのバージョンについて JsonSchemaは継続的に仕様追加や変更が行われており、 $schema キーワードで指定するURIで どのバージョンの仕様に準拠しているか 示す必要があります。 これにより、JsonSchemaを処理するライブラリやツールは、どのルールセットに基づいてスキーマを解釈し、バリデーションやコード生成を行うべきかを判断できます。 主要なJsonSchemaのバージョンとその説明を以下に記載します。 バージョン名 URI 説明 Draft 4 http://json-schema.org/draft-04/schema# 広く採用された初期のバージョンであり、現在のJsonSchemaの基礎部分が定義された。 Draft 7 http://json-schema.org/draft-07/schema# 現在、最も広く使われているバージョン。 if / then / else キーワードの追加により、複雑な条件に基づいたバリデーションが可能になった。 Draft 2020-12 https://json-schema.org/draft/2020-12/schema 最新安定版のバージョン。 $dynamicRef や $dynamicAnchor といったキーワードの追加で、より柔軟な再帰的参照が可能になるなど大規模なスキーマ定義がしやすくなった 実践: JsonSchemaを用いたバリデーション ここまで、JsonSchemaの基本的な概念と用途について説明しました。ここからは、Python のライブラリである Pydantic を用いて、実際のアプリケーションにおける JsonSchemaを活用したバリデーション方法を紹介します。 Pydanticとは Pydantic は、 Pydanticモデル と呼ばれる、Pythonの型ヒントとその型定義に基づいたデータバリデーション機能を持つクラスを生成できるライブラリです。 Pydanticモデルは型ヒントを最大限に活用するため、IDEの補完機能や、 mypy などの静的型チェッカーと連携できます。これにより、単なるデータのバリデーションに留まらず、コード記述時からミスを減らすことで、開発者の工数削減が期待できます。 JsonSchemaからPydanticモデルを自動生成する Pydanticモデルは手動で記述することもできますが、JsonSchemaからの自動生成が可能です。 今回、自動生成を datamodel-code-generator というツールを使用して行っていきます。本ツールは、JsonSchemaやSwaggerといったスキーマ定義から、Pydanticモデル(またはその他のデータクラス)を自動生成するためのコマンドラインツールです。 ※なお、本ツールは、 Pydanticの公式ドキュメント でも紹介されています (補足): Pydantic vs. JsonSchema Library PythonでJsonSchemaを使ったバリデーションを行うライブラリはPydanticだけではありません。例えば、 jsonschema (JSON Schema Library) というPythonライブラリも存在します。 それぞれの特徴の比較は、以下の通りです。 特徴 Pydantic JSON Schema Library JSON Schema標準への準拠 JSON Schemaのサブセットをサポートし、Pythonのデータモデルに最適化 JSON Schemaの標準仕様に厳密に準拠 パフォーマンス 高速(v2ではRustベース) 中程度(Pythonベース) 型安全性 Pythonの型ヒントに基づいてコードレベルでデータモデルの型チェックが可能 Pythonの辞書を直接扱うため、コード記述時のデータ構造の事前チェックは限定的 ライブラリの軽量性 多くの機能を持つため、比較的大規模。 検証に特化しているため、より軽量 学習コスト、依存関係 中程度(データモデルの知識が必要) 低い(JSON Schemaの知識のみ) 既存コード統合 導入に際して既存コードの改修が必要な場合がある 辞書ベースのデータをそのまま組み込み可能 カスタムバリデーション 豊富なバリデーター、カスタムロジック追加可 基本的な検証のみ エラーメッセージ 詳細で分かりやすい 基本的だが十分 実行時安全性 型エラーを事前検出 実行時エラーのリスク IDEサポート 補完・型チェック完全対応 辞書アクセスのみ データ変換 自動型変換・シリアライズ 検証のみ(変換機能なし) それぞれメリット・メリットがありますが、今回は、JsonSchemaから生成されるデータモデルをPythonのクラスとして扱いたい点、そして型安全性や開発効率のメリットを享受したい点から、Pydanticを用いてバリデーションを行っていきます。 datamodel-code-generator の導入と基本的な使い方 それでは、 datamodel-code-generator を導入し、実際にPydanticモデルを生成してみます 導入 pipを使ってインストールします。 pip install datamodel-code-generator 基本的な使い方 JsonSchemaの解説で用いた user_schema.json を例に、Pydanticモデルを生成してみます。 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/user.json", "title": "User", "description": "User profile data", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the user" }, "name": { "type": "string", "minLength": 1, "maxLength": 100, "description": "User Name" }, "email": { "type": "string", "format": "email", "description": "User email" }, "is_active": { "type": "boolean", "default": true, "description": "User active status. active is true" } }, "required": ["id", "name", "email"] } user_schema.json ファイルと同じディレクトリで、以下のコマンドを実行します。 datamodel-code-generator --input user_schema.json --input-file-type jsonschema --output user_models.py --input user_schema.json : 入力となるJsonSchemaファイルを指定します。 --input-file-type jsonschema : 入力ファイルの種別がJsonSchemaであることを明示します。 --output user_models.py : 生成されるPythonファイルの出力先を指定します。 コマンドを実行すると、 user_models.py というPythonファイルが生成されます。 # generated by datamodel-codegen: # filename: user_schema.json # timestamp: 2025-07-01T08:54:39+00:00 from __future__ import annotations from typing import Optional from uuid import UUID from pydantic import BaseModel, EmailStr, Field, constr class User(BaseModel): id: UUID = Field(..., description='Unique identifier for the user') name: constr(min_length=1, max_length=100) = Field(..., description='User Name') email: EmailStr = Field(..., description='User email') is_active: Optional[bool] = Field( True, description='User active status. active is true' ) JsonSchemaで定義したデータが、Pythonの Pydanticモデルのクラスに 変換されています。 description や min_length 、 max_length といったJsonSchemaのプロパティごとの制約が、Pydanticの Field 関数に適切にマッピングされているのが分かります。 アプリケーションへの組み込みとバリデーション実行 生成されたPydanticモデルを使い、実際のJSONデータのバリデーションを行ってみましょう。不正なデータが入力された場合に、Pydanticがどのようにエラーを検知し、更に詳細なエラーメッセージをJSON形式で出力することを確認します。 from pydantic import ValidationError from user_models import User import json # JSONデータの例 json_data = { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "name": "Alice", "email": "alice@example.com", "is_active": True, } try: # JSONデータをPydanticモデルにパース(バリデーションも自動実行) user = User.model_validate(json_data) print("データは正常にパースされました。") print(user.model_dump_json(indent=2)) # 型ヒントの恩恵を受ける print(f"\\nユーザーの名前: {user.name}") # 不正なデータでのバリデーションエラー invalid_json_data = { "id": "invalid-uuid", # 無効なUUID "name": "", # 最小文字数違反 "is_active": "invalid-boolean" # パターン違反 # emailは必須項目 } User.model_validate(invalid_json_data) except ValidationError as e: print("\\n▼ カスタムエラーメッセージ") error_messages = {} for error in e.errors(): # locタプルをドット区切りの文字列に変換 # (例: ('address', 'city') -> "address.city") field = ".".join(map(str, error['loc'])) message = error['msg'] error_messages[field] = message # 辞書をJSONとして出力 print(json.dumps(error_messages, indent=2, ensure_ascii=False)) コマンドラインでの実行と出力結果です。 python input.py データは正常にパースされました。 { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "name": "Alice", "email": "alice@example.com", "is_active": true } ユーザーの名前: Alice ▼ カスタムエラーメッセージ { "id": "Input should be a valid UUID, invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `i` at 1", "name": "String should have at least 1 character", "email": "Field required", "is_active": "Input should be a valid boolean, unable to interpret input" } この通り、JsonSchemaの定義に基づき、Pydanticが詳細かつ分かりやすいエラーメッセージを生成してくれました。これにより、どのフィールドでどのような問題が発生したのかを特定し、デバッグやエラーハンドリングを効率的に行うことができます。 実践: 動的カラムのバリデーション 次に、本記事の冒頭で触れた、PostgreSQLの jsonb 型カラムに保存されたユーザーごとに異なる形式のデータのバリデーションを行っていきます。 このJSONデータはキー名などに一貫性のないデータ形式であり、この課題を解決するため、JsonSchemaとPydanticを組み合わせた動的バリデーションのアプローチを導入します。 具体的には、ユーザーごとに異なるJsonSchemaをデータベースに保存し、アプリケーション実行時にそのJsonSchemaを読み込んでPydanticモデルを動的に生成し、バリデーションを行います。 ここでは、動的なデータの一例として、ユーザーの profile を表すJsonSchema(例: profile_schema.json )を定義します。このスキーマは、実際にはユーザーごとにDBに格納されるJsonSchemaを模したものとして扱います。 { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/profile.json", "title": "UserProfile", "description": "User profile data. This can vary from user to user.", "type": "object", "properties": { "hobbies": { "type": "array", "items": { "type": "string" }, "description": "List of user hobbies" }, "hometown": { "type": "string", "description": "User's hometown" }, "favorite_programming_languages": { "type": "array", "items": { "type": "string", "enum": ["Python", "JavaScript", "Java", "Go", "Other"] }, "description": "Favorite programming languages" } }, "required": ["hobbies"] } この profile_schema.json は、 hobbies が必須の文字列配列で、 favorite_programming_languages は特定のenum値のみを許容する文字列配列、といったルールを定義しています。 JsonSchemaを読み込み、Pydanticモデルを動的に生成する 今回のケースように、 ユーザーごとにスキーマが異なり、実行時に動的にスキーマを切り替えてバリデーションを行う 場合は、 datamodel-code-generator などでのPydanticモデルの事前生成はできません。 このようなケースでは、Pydanticライブラリの create_model 関数を用いることで、実行時にJsonSchemaを読み込み、その内容に基づいてPydanticモデルを動的に生成し、 model_validate 関数でバリデーションを行うことができます。 以下のコードでは、JsonSchemaの型定義をPythonの型にマッピングするユーティリティ関数を実装し、これを用いてJsonSchemaからPydanticモデルを生成します。 import json from pathlib import Path from typing import Any, Dict, List, Type from pydantic import BaseModel, create_model, ValidationError, EmailStr from uuid import UUID # JSON SchemaのタイプをPythonの型にマッピング SCHEMA_TYPE_MAP = { "string": str, "number": float, "integer": int, "boolean": bool, "array": List, "object": Dict, } # JSON Schemaのフォーマット指定を特定の型にマッピング FORMAT_TYPE_MAP = { "uuid": UUID, "email": EmailStr, } def schema_to_pydantic( schema: Dict[str, Any], model_name: str, exclude_fields: List[str] = None ) -> Type[BaseModel]: # フィールド定義を格納する辞書 fields = {} # スキーマからプロパティ一覧を取得 properties = schema.get("properties", {}) # 除外フィールドリストを初期化(Noneの場合は空リスト) exclude_fields = exclude_fields or [] # 各プロパティを順次処理してPydanticフィールドに変換 for key, prop in properties.items(): # 除外対象フィールドはスキップ if key in exclude_fields: continue if "type" in prop: # フォーマット指定がある場合は専用の型を使用(UUID、Emailなど) if prop.get("format") in FORMAT_TYPE_MAP: field_type = FORMAT_TYPE_MAP[prop["format"]] else: # 通常のタイプマッピングを適用 field_type = SCHEMA_TYPE_MAP.get(prop["type"], Any) # フィールドが必須かどうかを判定 is_required = key in schema.get("required", []) default_value = ... if is_required else prop.get("default") # フィールド定義を追加(型, デフォルト値) fields[key] = (field_type, default_value) # 動的にPydanticモデルクラスを生成して返す return create_model(model_name, **fields) def validate_schema_data( data: Dict[str, Any], model: Type[BaseModel] ) -> Dict[str, Any]: """ 独立したスキーマデータを検証する関数 """ validated_data = model.model_validate(data) return validated_data.model_dump(mode='json') def main(): # profile_schema.jsonファイルを読み込み try: profile_schema = json.loads(Path("profile_schema.json").read_text()) except FileNotFoundError as e: print(f"エラー: {e}") return # profile_schemaのPydanticモデルを動的生成 ProfileModel = schema_to_pydantic( profile_schema, profile_schema.get("title", "UserProfile")) print("✅ 動的Pydanticモデル生成完了") print(f"モデル名: {ProfileModel.__name__}") print(f"モデルフィールド: {list(ProfileModel.model_fields.keys())}") # テストケース1: 正常なプロファイルデータの検証 valid_profile_data = { "hobbies": ["プログラミング", "読書", "映画鑑賞"], "hometown": "東京都", "favorite_programming_languages": ["Python", "JavaScript", "Go"] } try: validated_profile_data = validate_schema_data( valid_profile_data, ProfileModel) print("\\n✅ テストケース1: 正常プロファイルデータテスト成功") print(json.dumps(validated_profile_data, indent=2, ensure_ascii=False)) except ValidationError as e: print("❌ 予期しないエラー") print(e) # テストケース2: 不正なプロファイルデータの検証(必須フィールド不足・不正値) invalid_profile_data = { "hometown": "大阪府", "favorite_programming_languages": ["InvalidLanguage", "Python"] # hobbiesが不足している(必須フィールド) } try: validate_schema_data(invalid_profile_data, ProfileModel) print("❌ エラーが発生すべきでした") except ValidationError as e: print("\\n✅ テストケース2: 不正プロファイルデータテスト成功") print("▼ エラーメッセージ") # エラーメッセージを整理して表示 error_messages = {} for error in e.errors(): field = ".".join(map(str, error['loc'])) error_messages[field] = error['msg'] print(json.dumps(error_messages, indent=2, ensure_ascii=False)) if __name__ == "__main__": main() 出力結果は以下になります。 python input.py ✅ 動的Pydanticモデル生成完了 モデル名: UserProfile モデルフィールド: ['hobbies', 'hometown', 'favorite_programming_languages'] ✅ テストケース1: 正常プロファイルデータテスト成功 { "hobbies": [ "プログラミング", "読書", "映画鑑賞" ], "hometown": "東京都", "favorite_programming_languages": [ "Python", "JavaScript", "Go" ] } ✅ テストケース2: 不正プロファイルデータテスト成功 ▼ エラーメッセージ { "hobbies": "Field required" } おわりに 本記事では、 JsonSchema について、用途やバージョン、Pydanticモデルへの変換と活用方法について解説しました。 課題だったPostgreSQLの jsonb 型のような動的なカラムに保存される可変長のデータに対しても、JsonSchemaとPydanticの機能を組み合わせることで、実行時にスキーマを読み込んで動的にバリデーションを行い、柔軟なデータ構造を扱いながらもデータ品質とアプリケーションの信頼性を確保することが出来ました。 JsonSchemaは、JSONデータの信頼性を保証し、開発プロセスの様々な面の効率化もできる便利な定義だと実感したため、同じような悩みを持つ方は、ぜひ一度本記事で紹介した内容を試してみてください。 なお、次回は複数のアプリケーションで利用する共通のスキーマ定義やその管理方法など、さらにJsonSchemaを用いた実践的な内容ついて記事を書いていこうと思います。 どうぞよろしくお願いします。 The post 【実践】JsonSchemaとPydantic – 自由なJSONデータで動的バリデーションを実現する first appeared on Sqripts .