
こんにちは、エンジニアのタカです。
今回は、私が直近で開発業務で使用している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を用いた実践的な内容ついて記事を書いていこうと思います。
どうぞよろしくお願いします。