Python Pydantic入門:データバリデーションを簡単に
データはアプリケーションの生命線です。Webアプリケーションにおけるリクエストデータ、データベースから取得したデータ、設定ファイルからのデータなど、様々な形式でデータはシステム内を流れています。しかし、これらのデータが常に期待通りの形式や値を持っているとは限りません。形式が間違っていたり、値が範囲外だったり、必須のフィールドが欠けていたりすると、予期しないエラーやセキュリティ上の問題を引き起こす可能性があります。
このような問題を未然に防ぐために不可欠なのがデータバリデーション(データ検証)です。入力されたデータが正しい形式、型、値の範囲を満たしているかを確認し、不正なデータを早期に検出して処理を拒否したり、エラーを通知したりします。
Pythonには様々なデータバリデーションの方法がありますが、近年特に注目を集め、多くのPythonプロジェクト、特にWebフレームワーク(FastAPIなど)で標準的に利用されているのがPydanticライブラリです。
この記事では、Python Pydanticの基本的な使い方から、より高度な機能、そして実際のアプリケーションでの活用方法まで、詳細に解説します。Pydanticを使うことで、いかにデータバリデーションが簡単かつ効率的に行えるようになるかを理解し、ぜひあなたのプロジェクトに取り入れてみてください。
1. Pydanticとは何か?なぜPydanticを使うのか?
1.1 Pydanticとは
Pydanticは、Pythonの型ヒント(Type Hinting)を利用してデータバリデーションと設定管理を行うライブラリです。PEP 484で導入された標準の型ヒント構文(str
, int
, List[str]
, Optional[int]
など)を使ってデータの構造と期待される型を定義するだけで、強力なバリデーション機能を自動的に提供します。
Pydanticの最大の特徴は、以下の点です。
- 型ヒントベース: 標準のPython型ヒントを利用するため、学習コストが低く、コードの可読性が高いです。
- 高速: Rustで実装されたコア部分(
pydantic-core
)を使用しており、非常に高速なデータパースとバリデーションが可能です。 - 豊富な機能: 基本的な型チェックだけでなく、複雑なネストされたデータ構造、カスタムバリデーション、設定管理、JSON Schema生成など、多くの機能を提供します。
- IDEサポート: 型ヒントを利用するため、IDEによる補完や静的解析の恩恵を受けやすいです。
1.2 なぜデータバリデーションが必要か
データバリデーションは、アプリケーションの信頼性と堅牢性を確保するために不可欠です。
- バグの防止: 不正なデータ形式や値によって発生するランタイムエラーを防ぎます。
- セキュリティの向上: 悪意のある入力を検出・拒否することで、SQLインジェクションやクロスサイトスクリプティング(XSS)などの攻撃を防ぐ一助となります。
- データの整合性の維持: データベースや他のシステムに書き込む前にデータの正確性を保証します。
- APIの契約: APIのエンドポイントが期待するデータ形式を明確に定義し、クライアントとサーバー間の契約を明確にします。
- 可読性と保守性の向上: データの期待される構造がコード上で明確になるため、他の開発者がコードを理解しやすくなります。
1.3 Pydanticを使うメリット
Pydanticをデータバリデーションに利用することには、多くのメリットがあります。
- コード量の削減: 手作業でバリデーションコードを書く代わりに、型ヒントとPydanticモデルを定義するだけで済むため、ボイラープレートコードが大幅に削減されます。
- バリデーションの一元化: データの定義とバリデーションルールが同じ場所に記述されるため、管理が容易になります。
- 自動的なデータ変換: バリデーション時に可能な範囲でデータの型変換(例:
"123"
文字列を123
整数に)を行ってくれます。 - 明確なエラー報告: バリデーションに失敗した場合、どのフィールドでどのようなエラーが発生したかを詳細かつ構造化された形式で報告してくれます。
- 開発効率の向上: 上記のメリットにより、開発速度が向上し、より重要なビジネスロジックに集中できます。
Pydanticは、特にWeb APIの開発(FastAPIとの組み合わせが有名)や、複雑なデータ処理を行うアプリケーションでその真価を発揮します。
2. Pydanticの基本
まずは、Pydanticを使うための基本的なステップを見ていきましょう。
2.1 インストール
Pydanticはpipを使って簡単にインストールできます。通常は標準版で十分ですが、より高速なV2の機能を使うためにはpydantic[standard]
またはpydantic[email]
などの追加パッケージをインストールすることも推奨されます。V2ではpydantic-core
がRust実装されており、高速化されています。
“`bash
pip install pydantic
または V2の推奨インストール方法 (pydantic-core, email-validatorなどが含まれる)
pip install pydantic[standard]
“`
この記事では、特に断りがない限り、Pydantic V2を前提として説明を行います。
2.2 BaseModel
の基本
Pydanticでデータモデルを定義する際の基本となるのが、pydantic
モジュールからインポートするBaseModel
クラスです。BaseModel
を継承したクラス内で、通常のクラス属性としてフィールドとその型ヒントを定義します。
“`python
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
is_active: bool = True # デフォルト値を設定
“`
このコードでは、User
というPydanticモデルを定義しています。このモデルは、以下の3つのフィールドを持つことを期待します。
name
: 文字列(str
)age
: 整数(int
)is_active
: 真偽値(bool
)。デフォルト値としてTrue
が設定されています。デフォルト値を持つフィールドは、データ入力時に省略可能です。
2.3 データのパースとバリデーション
Pydanticモデルの最も基本的な使い方は、入力データをモデルに渡してインスタンスを作成することです。このインスタンス作成の過程で、Pydanticが自動的にデータのバリデーションと型変換を行います。
モデルにデータを渡す方法はいくつかありますが、最も一般的なのは辞書形式で渡す方法です。
“`python
from pydantic import BaseModel, ValidationError
class User(BaseModel):
name: str
age: int
is_active: bool = True
正常なデータ例
data_ok = {
“name”: “Alice”,
“age”: 30
}
データをパースしてUserモデルのインスタンスを作成
try:
user_instance = User(**data_ok) # 辞書を展開してキーワード引数として渡す
print(user_instance)
print(f”Name: {user_instance.name}, Type: {type(user_instance.name)}”)
print(f”Age: {user_instance.age}, Type: {type(user_instance.age)}”)
print(f”Is Active: {user_instance.is_active}, Type: {type(user_instance.is_active)}”)
except ValidationError as e:
print(f”バリデーションエラー: {e}”)
print(“-” * 20)
型変換されるデータ例
data_type_coercion = {
“name”: “Bob”,
“age”: “25”, # 年齢が文字列として入力されている
“is_active”: “False” # 真偽値が文字列として入力されている
}
try:
user_instance_coercion = User(**data_type_coercion)
print(user_instance_coercion)
print(f”Name: {user_instance_coercion.name}, Type: {type(user_instance_coercion.name)}”)
print(f”Age: {user_instance_coercion.age}, Type: {type(user_instance_coercion.age)}”) # int型に変換される
print(f”Is Active: {user_instance_coercion.is_active}, Type: {type(user_instance_coercion.is_active)}”) # bool型に変換される
except ValidationError as e:
print(f”バリデーションエラー: {e}”)
“`
解説:
User(**data_ok)
のように辞書を**
で展開して渡すと、辞書のキーがキーワード引数として渡されます。Pydanticはこれを受け取り、対応するフィールドに値を割り当てながらバリデーションを行います。- 正常なデータ
data_ok
の場合、User
インスタンスが正常に作成され、各フィールドの値にアクセスできます。is_active
はデータに含まれていませんが、デフォルト値のTrue
が使われます。 data_type_coercion
の場合、age
とis_active
が文字列で渡されていますが、Pydanticは賢くこれを対応する型(int
,bool
)に変換しようとします。この変換に成功した場合、バリデーションは通過します。このように、Pydanticは可能な範囲で型変換(coercion)を行います。
2.4 エラーハンドリング
バリデーションに失敗した場合、Pydanticはpydantic.ValidationError
例外を発生させます。この例外を捕捉することで、エラー情報を取得し、適切に処理することができます。
“`python
from pydantic import BaseModel, ValidationError
class User(BaseModel):
name: str
age: int
is_active: bool = True
不正なデータ例 (ageが文字列、nameが欠けている)
data_invalid = {
“age”: “thirty” # intに変換できない文字列
# nameが欠けている
}
try:
user_instance_invalid = User(**data_invalid)
print(user_instance_invalid)
except ValidationError as e:
print(“バリデーションエラーが発生しました:”)
print(e) # エラーメッセージ全体を表示
print(“-” * 20)
# エラーの詳細情報を取得
errors = e.errors()
print(“エラー詳細:”)
for error in errors:
print(f” 場所 (フィールド): {error[‘loc’]}”)
print(f” メッセージ: {error[‘msg’]}”)
print(f” タイプ: {error[‘type’]}”)
print(“-” * 10)
“`
解説:
data_invalid
は、必須フィールドであるname
が欠けており、age
の値も整数に変換できない文字列です。User(**data_invalid)
の呼び出しでValidationError
が発生します。try...except ValidationError
ブロックでこの例外を捕捉します。- 例外オブジェクト
e
は、エラーメッセージ全体を表示するだけでなく、.errors()
メソッドを使ってエラーの詳細なリストを取得できます。 .errors()
メソッドは、エラーごとの辞書のリストを返します。各辞書には、エラーが発生したフィールドのパス(loc
)、エラーメッセージ(msg
)、エラータイプ(type
)などが含まれています。この構造化されたエラー情報は、APIのエラーレスポンスなどで非常に役立ちます。
このセクションで見たように、Pydanticを使えば、型ヒントを使ってデータの構造を定義し、辞書などの入力データを渡すだけで、自動的に強力なバリデーションと型変換が行われます。エラー発生時には詳細な情報を取得できるため、エラーハンドリングも容易です。
3. 基本的なフィールドタイプ
PydanticはPythonの標準型ヒントをそのまま利用できます。ここでは、よく使う基本的な型について説明します。
3.1 プリミティブ型
Pythonの基本的なプリミティブ型(文字列、整数、浮動小数点数、真偽値)は、そのまま型ヒントとして使用できます。
“`python
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: float
stock: int
is_available: bool
data = {
“name”: “Laptop”,
“price”: 1200.50,
“stock”: 50,
“is_available”: True
}
product = Product(**data)
print(product)
“`
Pydanticは、これらの型に対して適切なバリデーションを行います。例えば、stock: int
に対して文字列や浮動小数点数を渡すと、可能な場合は型変換を行い、不可能な場合はエラーとします。
“`python
from pydantic import BaseModel, ValidationError
class Product(BaseModel):
name: str
price: float
stock: int
is_available: bool
data_invalid = {
“name”: “Tablet”,
“price”: “500.99”, # 文字列だがfloatに変換可能
“stock”: 25.5, # floatだがintに変換可能 (小数点以下切り捨てではないので注意が必要な場合も)
“is_available”: “Yes” # boolに変換できない文字列
}
try:
product_invalid = Product(**data_invalid)
print(product_invalid)
except ValidationError as e:
print(e.errors())
“`
この例では、price
はfloatに、stock
はintに変換されますが、is_available
の"Yes"
はboolに変換できないためバリデーションエラーとなります。Pydanticは、"true"
, "false"
, "on"
, "off"
, "1"
, "0"
, True
, False
, 1
, 0
などをboolとして認識します(大文字小文字は区別しない)。
3.2 リスト、辞書、セット
Pythonのコレクション型(list
, dict
, set
)も型ヒントと組み合わせて使用できます。typing
モジュールから適切な型をインポートする必要があります。
“`python
from pydantic import BaseModel, ValidationError
from typing import List, Dict, Set, Tuple
class Item(BaseModel):
id: int
name: str
class Order(BaseModel):
order_id: str
items: List[Item] # Itemモデルのリスト
tags: Set[str] # 文字列のセット
metadata: Dict[str, str] # キーも値も文字列の辞書
coordinates: Tuple[float, float] # 2つのfloatを含むタプル
data = {
“order_id”: “ORD123”,
“items”: [
{“id”: 1, “name”: “Laptop”},
{“id”: 2, “name”: “Mouse”}
],
“tags”: [“electronics”, “computer”, “sale”, “electronics”], # リストだがセットに変換される
“metadata”: {
“customer_id”: “C101”,
“priority”: “high”
},
“coordinates”: [135.0, 35.0] # リストだがタプルに変換される
}
order = Order(**data)
print(order)
print(f”Tags type: {type(order.tags)}”)
print(f”Coordinates type: {type(order.coordinates)}”)
不正なデータ例
data_invalid = {
“order_id”: “ORD456”,
“items”: [
{“id”: 3, “name”: “Keyboard”},
{“id”: “four”, “name”: “Monitor”} # itemsリスト内の要素が不正 (idがintでない)
],
“tags”: “not a set”, # セットでない
“metadata”: {“key”: 123}, # 値が文字列でない
“coordinates”: [1.0, 2.0, 3.0] # 要素数が不正なタプル
}
try:
order_invalid = Order(**data_invalid)
except ValidationError as e:
print(e.errors())
“`
解説:
List[Item]
のように、コレクション型の中にPydanticモデルや他の型を指定することで、ネストされた構造のバリデーションも自動的に行われます。Set[str]
は文字列のセットを期待します。入力がリストであっても、Pydanticはそれをセットに変換し、重複を排除します。Dict[str, str]
はキーも値も文字列である辞書を期待します。Tuple[float, float]
は厳密に2つのfloat要素を持つタプルを期待します。入力がリストであってもタプルに変換しようとしますが、要素数が異なるとエラーになります。- エラー例では、
items
リスト内の要素、tags
の型、metadata
の値の型、coordinates
の要素数など、様々な場所でのバリデーションエラーが捕捉されています。エラーメッセージのloc
フィールドを見ると、エラーが発生したパス(例:('items', 1, 'id')
)が明確に示されていることがわかります。
3.3 Union、Optional
データが複数の型のうちいずれかを取りうる場合や、フィールドが任意(値が存在しない可能性がある)である場合は、typing
モジュールからUnion
やOptional
を使用します。
Union[TypeA, TypeB, ...]
:指定された型のいずれかにマッチします。Optional[Type]
:これはUnion[Type, NoneType]
の糖衣構文(シンタックスシュガー)です。つまり、その型の値またはNone
を受け入れます。
“`python
from pydantic import BaseModel
from typing import Union, Optional
class Profile(BaseModel):
# ageはintまたはNoneの可能性がある
age: Optional[int] = None # デフォルト値Noneを設定することが多い
# contact_infoは文字列(電話番号など)またはint(メッセージIDなど)の可能性がある
contact_info: Union[str, int]
# statusは文字列またはNoneの可能性がある (Optional[str]と同じ)
status: Union[str, None] = "pending"
data_optional_union_ok = {
“age”: 25,
“contact_info”: “090-1234-5678”,
“status”: “active”
}
profile_ok = Profile(**data_optional_union_ok)
print(profile_ok)
data_optional_union_none = {
“age”: None, # Optional[int]なのでNoneはOK
“contact_info”: 12345, # Union[str, int]なのでintはOK
“status”: None # Union[str, None]なのでNoneはOK
}
profile_none = Profile(**data_optional_union_none)
print(profile_none)
data_optional_union_invalid = {
“age”: “twenty”, # Optional[int]だがintにもNoneにも変換できない
“contact_info”: [1, 2], # Union[str, int]だがstrにもintにも変換できない
# statusが欠けているが、デフォルト値があるため問題なし
}
try:
profile_invalid = Profile(**data_optional_union_invalid)
except ValidationError as e:
print(e.errors())
“`
解説:
Optional[int]
はint
またはNone
を受け入れます。デフォルト値にNone
を設定しておくと、フィールドが省略された場合もエラーになりません。Union[str, int]
はstr
またはint
のいずれかを受け入れます。- Pydanticは
Union
型の場合、指定された型の順番にバリデーションを試みます。したがって、より具体的な型や、変換にコストがかかる型を先に指定すると効率が良い場合があります。
3.4 Any
typing.Any
を使用すると、どのような型の値も受け入れるフィールドを定義できます。これは、データの型が事前に分からない場合や、厳密な型チェックを行いたくない場合に便利ですが、Pydanticの強力な型バリデーションのメリットを損なうため、可能な限り具体的な型を指定することが推奨されます。
“`python
from pydantic import BaseModel
from typing import Any
class Config(BaseModel):
setting_name: str
setting_value: Any # どんな値でも受け入れる
data_any = {
“setting_name”: “log_level”,
“setting_value”: “INFO” # str, int, bool, listなど何でもOK
}
config = Config(**data_any)
print(config)
data_any_2 = {
“setting_name”: “feature_flags”,
“setting_value”: {“feat_a”: True, “feat_b”: False}
}
config_2 = Config(**data_any_2)
print(config_2)
“`
4. より高度なフィールドタイプ
PydanticはPython標準の型だけでなく、日付・時刻、UUID、列挙型などの特殊な型や、特定の形式(メールアドレス、URLなど)をバリデーションするための専用の型を提供しています。また、正規表現を用いたより詳細なバリデーションも可能です。
4.1 Datetime, Date, Time, Timedelta
日付や時刻に関連する型は、Python標準ライブラリのdatetime
モジュールからインポートして使用します。Pydanticは文字列形式の日付/時刻を自動的にパースしようとします。
“`python
from pydantic import BaseModel, ValidationError
from datetime import datetime, date, time, timedelta
class Event(BaseModel):
event_name: str
start_time: datetime
event_date: date
duration: timedelta
schedule_time: time # 時間のみ
data = {
“event_name”: “Meeting”,
“start_time”: “2023-10-27T10:30:00Z”, # ISO 8601形式の文字列
“event_date”: “2023-10-27”, # YYYY-MM-DD形式の文字列
“duration”: “PT1H30M”, # ISO 8601 duration形式の文字列 (timedeltaに変換)
“schedule_time”: “14:00:00” # HH:MM:SS形式の文字列
}
event = Event(**data)
print(event)
print(f”Start time type: {type(event.start_time)}”)
print(f”Event date type: {type(event.event_date)}”)
print(f”Duration type: {type(event.duration)}”)
print(f”Schedule time type: {type(event.schedule_time)}”)
不正なデータ例
data_invalid = {
“event_name”: “Invalid Event”,
“start_time”: “not a datetime”, # パースできない文字列
“event_date”: “Oct 27 2023”, # 不正な日付形式
“duration”: “2 hours”, # パースできない形式
“schedule_time”: “noon” # パースできない形式
}
try:
event_invalid = Event(**data_invalid)
except ValidationError as e:
print(e.errors())
“`
PydanticはISO 8601形式の文字列を特に得意としますが、他の一般的な形式もある程度パースできます。ただし、厳密な形式を強制したい場合は、カスタムバリデーションを検討する必要があります。
4.2 UUID
一意な識別子としてよく使われるUUIDも、uuid
モジュールからUUID
型をインポートして使用できます。Pydanticはハイフンあり/なしの文字列形式をUUIDオブジェクトに変換します。
“`python
from pydantic import BaseModel, ValidationError
from uuid import UUID, uuid4
class Resource(BaseModel):
resource_id: UUID
data_uuid = {
“resource_id”: “a1b2c3d4-e5f6-7890-1234-567890abcdef” # ハイフンあり文字列
}
resource = Resource(**data_uuid)
print(resource)
print(f”Resource ID type: {type(resource.resource_id)}”)
data_uuid_no_hyphen = {
“resource_id”: “a1b2c3d4e5f678901234567890abcdef” # ハイフンなし文字列
}
resource_no_hyphen = Resource(**data_uuid_no_hyphen)
print(resource_no_hyphen)
不正なデータ例
data_uuid_invalid = {
“resource_id”: “not a uuid” # 不正な形式
}
try:
resource_invalid = Resource(**data_uuid_invalid)
except ValidationError as e:
print(e.errors())
“`
4.3 Enum
特定の固定された値のセットのみを受け入れたい場合は、Python標準ライブラリのenum
モジュールからEnum
を使用できます。
“`python
from pydantic import BaseModel, ValidationError
from enum import Enum
class StatusEnum(str, Enum): # strを継承すると、文字列値として扱える
pending = “pending”
active = “active”
inactive = “inactive”
closed = “closed”
class Task(BaseModel):
task_id: int
status: StatusEnum # Enum型のフィールド
data_enum_ok = {
“task_id”: 101,
“status”: “active” # Enumメンバーの値(文字列)で渡す
}
task_ok = Task(**data_enum_ok)
print(task_ok)
print(f”Status type: {type(task_ok.status)}”)
print(f”Status value: {task_ok.status.value}”) # .valueで基底の文字列値を取得
data_enum_invalid = {
“task_id”: 102,
“status”: “in_progress” # Enumに存在しない値
}
try:
task_invalid = Task(**data_enum_invalid)
except ValidationError as e:
print(e.errors())
“`
Enum
を使うことで、取りうる値の範囲を明確に制限できます。str
を継承することで、入力としてEnumメンバーの値を文字列として受け入れ、Pydanticは自動的に対応するEnumメンバーに変換してくれます。
4.4 カスタマイズ可能なフィールド (e.g., EmailStr, HttpUrl)
Pydanticは、特定の形式を持つ文字列をバリデーションするための特別な型を提供しています。これらの型は、pydantic
モジュールからインポートできます。
“`python
from pydantic import BaseModel, ValidationError, EmailStr, HttpUrl, PastDate
class UserProfile(BaseModel):
email: EmailStr # 有効なメールアドレス形式をチェック
website: Optional[HttpUrl] = None # 有効なURL形式をチェック (任意)
birth_date: PastDate # 過去の日付であることをチェック (v2のアノテーション)
data_special_types_ok = {
“email”: “[email protected]”,
“website”: “https://www.example.com/path?query=1”,
“birth_date”: “1990-05-15”
}
profile_special = UserProfile(**data_special_types_ok)
print(profile_special)
print(f”Email type: {type(profile_special.email)}”)
print(f”Website type: {type(profile_special.website)}”)
print(f”Birth date type: {type(profile_special.birth_date)}”)
data_special_types_invalid = {
“email”: “invalid-email”, # 不正なメールアドレス形式
“website”: “not a url”, # 不正なURL形式
“birth_date”: “2050-01-01” # 未来の日付
}
try:
profile_special_invalid = UserProfile(**data_special_types_invalid)
except ValidationError as e:
print(e.errors())
“`
解説:
EmailStr
: 入力文字列が基本的なメールアドレスの形式([email protected]
など)に合致するかをチェックします。より厳密なチェックを行うには、追加のパッケージ(例:email-validator
,idna
)のインストールが必要な場合があります(通常pydantic[standard]
に含まれます)。HttpUrl
: 入力文字列が有効なHTTPまたはHTTPSのURL形式に合致するかをチェックします。PastDate
,FutureDate
,PastDatetime
,FutureDatetime
: Pydantic v2で導入されたアノテーション型で、日付や日時が過去または未来であるかをチェックします。
これらの特殊な型を利用することで、一般的な形式のバリデーションを手軽に行えます。
4.5 正規表現を使ったバリデーション (constr, conint, conlistなど)
Pydantic v1では、pydantic.types
モジュールにconstr
, conint
, conlist
などの型があり、これらを使って最小値/最大値、長さ、正規表現パターンなどを指定できました。
Pydantic v2では、これらの機能の多くがpydantic.fields
モジュールにあるField
関数に統合され、より統一的な方法でフィールドの設定や制約を行うようになりました。
V2では、型ヒントに制約を直接加えることはせず、Field
関数を使って制約を定義します。
“`python
from pydantic import BaseModel, ValidationError, Field
import re
class Item(BaseModel):
# 文字列: 最小長さ1, 最大長さ50, アルファベットと数字のみ (正規表現)
item_code: str = Field(…, min_length=1, max_length=50, pattern=r”^[a-zA-Z0-9]+$”)
# 整数: 0以上 100以下
quantity: int = Field(..., ge=0, le=100) # ge: Greater than or equal to, le: Less than or equal to
# リスト: 要素数は1以上 10以下
tags: list[str] = Field(..., min_length=1, max_length=10) # 型ヒントはlist[str]でOK
# 浮動小数点数: 0.0より大きい
price: float = Field(..., gt=0.0) # gt: Greater than
# フィールドの説明や例を追加 (ドキュメント生成などに利用)
description: str = Field("", description="商品の説明", examples=["これは素晴らしい商品です"])
… は必須フィールドであることを示すプレースホルダー
data_constrained_ok = {
“item_code”: “ABC123”,
“quantity”: 50,
“tags”: [“electronics”, “gadget”],
“price”: 99.99,
“description”: “A high-quality gadget.”
}
item_constrained = Item(**data_constrained_ok)
print(item_constrained)
data_constrained_invalid = {
“item_code”: “Item Code With Spaces!”, # 正規表現パターンにマッチしない
“quantity”: 150, # 100より大きい
“tags”: [], # 要素数が0
“price”: -10.0, # 0.0より小さい
“description”: “Short description” # 例は必須ではない
}
try:
item_constrained_invalid = Item(**data_constrained_invalid)
except ValidationError as e:
print(e.errors())
“`
解説:
Field
関数は、フィールドのデフォルト値(または必須を示す...
)、エイリアス、説明、例、そして様々なバリデーション制約を指定するために使用します。min_length
,max_length
: 文字列やリストなどの長さに関する制約。pattern
: 文字列が指定された正規表現にマッチする必要があるという制約。ge
,le
,gt
,lt
: 数値の範囲に関する制約(>=, <=, >, <)。- これらの制約は、型ヒントによる基本的な型チェックに加えて適用されます。
Field(..., ...)
の最初の引数...
は、フィールドにデフォルト値がなく、必須であることを示します。デフォルト値を設定する場合は、Field(default_value, ...)
のように記述します。
Field
関数は、Pydantic V2におけるフィールド定義の中心的な役割を担います。これにより、型の指定、デフォルト値、制約、メタデータなどを一箇所で管理できるようになりました。
5. カスタムバリデーション
Pydanticが提供する基本的な型や制約だけでは不十分な場合があります。例えば、「パスワードは特定の文字列を含まなければならない」「開始日より終了日が後であること」のような、複数のフィールドにまたがる、あるいは独自の複雑なロジックに基づくバリデーションを行いたい場合です。
Pydanticでは、Pythonのメソッドを使ってカスタムバリデーションロジックを定義できます。
5.1 フィールドバリデーター (validator
デコレーター – Pydantic v1系)
Pydantic v1では、特定のフィールドの値に対してカスタムなチェックを行うために、@validator
デコレーターを使ってクラスメソッドを定義しました。
“`python
Pydantic v1 の例 (v2では推奨されない)
from pydantic import BaseModel, validator, ValidationError
class UserV1(BaseModel):
username: str
password: str
@validator('username') # usernameフィールドに対するバリデーター
def validate_username(cls, value):
if not value.isalnum():
raise ValueError('Username must be alphanumeric')
return value # バリデーション後の値を返す
@validator('password') # passwordフィールドに対するバリデーター
def validate_password(cls, value):
if len(value) < 8:
raise ValueError('Password must be at least 8 characters long')
if not any(c.isupper() for c in value):
raise ValueError('Password must contain at least one uppercase letter')
return value
v1モデルでのバリデーション
try:
user_ok_v1 = UserV1(username=”john_doe”, password=”Password123″)
print(user_ok_v1)
except ValidationError as e:
print(“V1 エラー:”, e.errors())
try:
user_invalid_v1 = UserV1(username=”john doe”, password=”password”)
print(user_invalid_v1)
except ValidationError as e:
print(“V1 エラー:”, e.errors())
“`
@validator('フィールド名')
デコレーターは、指定されたフィールドの値を引数として受け取り、バリデーションを行います。バリデーションが成功した場合は、その値を返す必要があります。失敗した場合はValueError
やTypeError
などの例外を発生させます。
Pydantic V2でのフィールドバリデーション:
Pydantic V2では、@validator
は非推奨となり、代わりにpydantic.field_validator
デコレーターを使用します。このデコレーターは、バリデーション対象のフィールド名を文字列で指定するのではなく、バリデーターメソッド名や型ヒントから自動的に対象フィールドを推論するか、*
を使って全てのフィールドを指定するなどの方法で対象を指定します。
“`python
Pydantic v2
from pydantic import BaseModel, field_validator, ValidationError
class UserV2(BaseModel):
username: str
password: str
@field_validator('username') # usernameフィールドに対するバリデーター
@classmethod
def validate_username(cls, value):
if not value.isalnum():
raise ValueError('Username must be alphanumeric')
return value
@field_validator('password') # passwordフィールドに対するバリデーター
@classmethod
def validate_password(cls, value):
if len(value) < 8:
raise ValueError('Password must be at least 8 characters long')
if not any(c.isupper() for c in value):
raise ValueError('Password must contain at least one uppercase letter')
return value
v2モデルでのバリデーション
try:
user_ok_v2 = UserV2(username=”jane_doe”, password=”SecurePassword456″)
print(user_ok_v2)
except ValidationError as e:
print(“V2 エラー:”, e.errors())
try:
user_invalid_v2 = UserV2(username=”jane doe”, password=”short”)
print(“V2 エラー:”, e.errors()) # エラーが発生するはず
except ValidationError as e:
print(“V2 エラー:”, e.errors())
“`
V2の@field_validator
もv1の@validator
と同様に機能しますが、クラスメソッドであること、デコレーターへの引数の渡し方などに違いがあります。特に、@field_validator
はデフォルトではバリデーション対象のフィールド名をメソッド名から推論しようとします(例: validate_username
というメソッド名ならusername
フィールド)。明示的に指定したい場合は、v1と同様に文字列でフィールド名を渡します。複数のフィールドに同じバリデーターを適用したい場合は、デコレーターに複数のフィールド名を渡すか、*
を指定します。
5.2 ルートバリデーター (root_validator
デコレーター – Pydantic v1系)
Pydantic v1では、モデル全体のデータ(全てのフィールドを含む辞書形式)に対してバリデーションを行いたい場合に、@root_validator
デコレーターを使いました。これは主に、複数のフィールド間の関係をチェックするのに使用されました。
“`python
Pydantic v1 の例 (v2では推奨されない)
from pydantic import BaseModel, root_validator, ValidationError
from datetime import date
class DateRangeV1(BaseModel):
start_date: date
end_date: date
@root_validator # モデル全体に対するバリデーター
def check_dates(cls, values):
start = values.get('start_date')
end = values.get('end_date')
if start and end and start > end:
raise ValueError('End date must be after start date')
return values # バリデーション後の値を返す
v1モデルでのバリデーション
data_v1 = {“start_date”: “2023-10-30”, “end_date”: “2023-10-28”} # 不正な順序
try:
date_range_v1 = DateRangeV1(**data_v1)
print(date_range_v1)
except ValidationError as e:
print(“V1 エラー:”, e.errors())
“`
@root_validator
デコレーターを付けたクラスメソッドは、パース中の全てのデータを含む辞書(values
引数)を受け取ります。バリデーションが成功した場合は、この辞書を返す必要があります。失敗した場合はValueError
などを発生させます。
5.3 モデルバリデーター (model_validator
デコレーター – Pydantic v2系)
Pydantic V2では、@root_validator
は非推奨となり、代わりにpydantic.model_validator
デコレーターが導入されました。これは、モデル全体のバリデーションを行うためのデコレーターです。
@model_validator
は、バリデーションが実行されるフェーズに応じて、以下の2つのモードがあります。
- Before Validation (
mode='before'
): 入力データ(パース前の辞書など)を受け取り、それを変換してからPydanticの標準バリデーションに渡したい場合に使用します。 - After Validation (
mode='after'
): Pydanticの標準バリデーション(およびフィールドバリデーター)が完了し、全てのフィールドが適切な型に変換された後のモデルインスタンスを受け取り、フィールド間の関係などをチェックする場合に使用します。通常はこちらを使います。
フィールド間の関係チェックなど、v1の@root_validator
の主な用途は、v2の@model_validator(mode='after')
に置き換わります。
“`python
Pydantic v2
from pydantic import BaseModel, model_validator, ValidationError
from datetime import date
class DateRangeV2(BaseModel):
start_date: date
end_date: date
duration_days: int = 0
@model_validator(mode='after') # 全てのフィールドのバリデーション後に実行
def check_dates_and_duration(self): # インスタンスメソッドとして定義
if self.start_date and self.end_date and self.start_date > self.end_date:
raise ValueError('End date must be after start date')
# オプション: start_dateとend_dateからduration_daysを計算して設定 (またはチェック)
# timedeltaオブジェクトとして計算されるため、daysプロパティで日数を取得
delta = self.end_date - self.start_date
if self.duration_days == 0: # duration_daysが指定されていない場合、計算して設定
self.duration_days = delta.days
elif self.duration_days != delta.days: # 指定されている場合はチェック
# エラーにすることも、警告にすることも、無視することも可能
print(f"Warning: duration_days ({self.duration_days}) does not match calculated days ({delta.days})")
# raise ValueError(f'Duration days ({self.duration_days}) does not match date difference ({delta.days})')
return self # インスタンス自体を返す必要がある
v2モデルでのバリデーション
data_v2_invalid = {“start_date”: “2023-11-01”, “end_date”: “2023-10-31”} # 不正な順序
try:
date_range_v2_invalid = DateRangeV2(**data_v2_invalid)
print(date_range_v2_invalid)
except ValidationError as e:
print(“V2 エラー:”, e.errors())
print(“-” * 20)
data_v2_ok = {“start_date”: “2023-11-01”, “end_date”: “2023-11-10”} # 正しい順序
try:
date_range_v2_ok = DateRangeV2(**data_v2_ok)
print(date_range_v2_ok) # duration_daysが自動計算されている
except ValidationError as e:
print(“V2 エラー:”, e.errors())
print(“-” * 20)
data_v2_with_duration = {“start_date”: “2023-11-01”, “end_date”: “2023-11-10”, “duration_days”: 10} # duration_daysが指定されている
try:
date_range_v2_with_duration = DateRangeV2(**data_v2_with_duration)
print(date_range_v2_with_duration) # duration_daysが一致しているかチェック
except ValidationError as e:
print(“V2 エラー:”, e.errors())
print(“-” * 20)
data_v2_with_duration_mismatch = {“start_date”: “2023-11-01”, “end_date”: “2023-11-10”, “duration_days”: 5} # duration_daysが一致しない
try:
date_range_v2_with_duration_mismatch = DateRangeV2(**data_v2_with_duration_mismatch)
print(date_range_v2_with_duration_mismatch) # Warningが表示される
except ValidationError as e:
print(“V2 エラー:”, e.errors())
“`
解説:
@model_validator(mode='after')
: Pydanticの標準バリデーションが完了した後、生成されたモデルインスタンスを引数self
として受け取ります。mode='after'
の場合、メソッドはインスタンスメソッドとして定義する必要があります(self
を受け取る)。- バリデーション成功時は、インスタンス自身(
self
)を返す必要があります。 - このバリデーターの中で、
self.start_date
やself.end_date
のように、既にバリデーションされ適切な型になっているフィールドの値にアクセスできます。 - エラーが発生した場合は、
ValueError
などを発生させます。Pydanticはこれを捕捉し、ValidationError
として報告します。
@model_validator(mode='before')
は、例えば入力データがJSON文字列の場合に、辞書に変換したり、特定のキーの名前を変更したりするような、前処理的なバリデーション/変換に使われます。
カスタムバリデーションを適切に使うことで、Pydanticの機能を拡張し、アプリケーション固有の複雑なバリデーションルールを実現できます。
6. モデルのネスト
実際のアプリケーションでは、データはしばしば階層的な構造を持ちます。Pydanticは、BaseModel
を別のBaseModel
のフィールドの型として指定することで、簡単にネストされたデータ構造を表現し、バリデーションできます。
6.1 他のBaseModel
をフィールドとして使用する
“`python
from pydantic import BaseModel, ValidationError
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
zip_code: str
country: str = “Japan” # デフォルト値付き
class Company(BaseModel):
name: str
address: Address # Addressモデルをネスト
class Employee(BaseModel):
employee_id: int
name: str
company: Company # Companyモデルをネスト
address: Address # 従業員の住所 (Company.addressとは別のフィールド)
# オプションのフィールドとしてリスト内のネストも可能
previous_companies: Optional[List[Company]] = None
data_nested_ok = {
“employee_id”: 1001,
“name”: “Taro Yamada”,
“company”: {
“name”: “Tech Solutions Inc.”,
“address”: {
“street”: “1-2-3 Main St”,
“city”: “Tokyo”,
“zip_code”: “100-0001”
# countryは省略 -> “Japan”がデフォルト値として使われる
}
},
“address”: { # 従業員の住所
“street”: “4-5-6 Back Ln”,
“city”: “Osaka”,
“zip_code”: “540-0001”,
“country”: “Japan”
},
“previous_companies”: [
{
“name”: “Old Company”,
“address”: {
“street”: “7-8-9 Side Rd”,
“city”: “Kyoto”,
“zip_code”: “600-0001”
}
}
]
}
employee_ok = Employee(**data_nested_ok)
print(employee_ok)
print(f”Employee name: {employee_ok.name}”)
print(f”Company name: {employee_ok.company.name}”)
print(f”Company city: {employee_ok.company.address.city}”)
print(f”Employee city: {employee_ok.address.city}”)
if employee_ok.previous_companies:
print(f”Previous company name: {employee_ok.previous_companies[0].name}”)
print(“-” * 20)
不正なデータ例 (ネストされたモデル内のエラー)
data_nested_invalid = {
“employee_id”: 1002,
“name”: “Hanako Sato”,
“company”: {
“name”: “Consulting Co.”,
“address”: {
“street”: “Invalid St”,
# cityが欠けている (Addressモデルで必須)
“zip_code”: “123-4567”
}
},
“address”: { # 従業員の住所
“street”: “Valid Address”,
“city”: “Nagoya”,
“zip_code”: “460-0001”
# countryは省略 -> “Japan”がデフォルト値として使われる
},
“previous_companies”: [
{
“name”: “Another Old Company”,
“address”: {
“street”: “Valid Street”,
“city”: “Fukuoka”,
“zip_code”: 8100001 # int型だがstringに変換可能
}
},
{ # previous_companiesリスト内の要素が不正
“name”: “Yet Another Co.”,
“address”: “not an address object” # Addressモデルではない
}
]
}
try:
employee_invalid = Employee(**data_nested_invalid)
except ValidationError as e:
print(e.errors())
“`
解説:
Company
モデルはaddress: Address
フィールドを持ち、Employee
モデルはcompany: Company
とaddress: Address
フィールドを持ちます。このように、任意の深さでモデルをネストさせることができます。- Pydanticは、ネストされたモデルについても自動的にバリデーションを行います。入力データの辞書内にネストされた辞書やリストがある場合、Pydanticは対応するPydanticモデルのインスタンスを作成しようとします。
- エラー例を見ると、
company.address.city
が欠けているエラーと、previous_companies
リスト内の2番目の要素のaddress
フィールドが期待されるAddress
モデルの形式ではないというエラーが報告されています。エラーのloc
フィールドは、エラーが発生したデータの階層的なパスを示しており、デバッグに役立ちます。
ネストされたモデルを使うことで、複雑な階層構造を持つデータを、構造を明確に保ったままバリデーションできます。これは、APIのリクエスト/レスポンスデータの定義などで非常に一般的に使われるパターンです。
7. モデルの設定と設定クラス
Pydanticモデルの振る舞いは、設定オプションによって変更できます。これらの設定は、Pydantic V1では内部クラスConfig
を使って行われましたが、Pydantic V2ではクラス属性model_config
(辞書またはConfigDictオブジェクト)を使って行います。
ここでは、Pydantic V2の方法を中心に説明します。
7.1 model_config
属性 (v2系)
Pydantic V2では、モデルクラス内にmodel_config
というクラス属性を定義し、辞書形式で設定オプションを指定します。
“`python
from pydantic import BaseModel, ValidationError
class Product(BaseModel):
model_config = {
‘extra’: ‘forbid’, # モデルで定義されていないフィールドを許可しない
‘frozen’: True # モデルインスタンスのフィールドを不変にする
}
name: str
price: float
正常なデータ (定義されているフィールドのみ)
data_ok = {“name”: “Book”, “price”: 20.0}
product_ok = Product(**data_ok)
print(product_ok)
不正なデータ (extra=’forbid’ のため、定義されていないフィールドがあるとエラー)
data_invalid_extra = {“name”: “Chair”, “price”: 100.0, “color”: “red”} # colorはProductモデルに定義されていない
try:
product_invalid_extra = Product(**data_invalid_extra)
except ValidationError as e:
print(“Extra field error:”, e.errors())
frozen=True のため、インスタンス生成後にフィールドの値を変更しようとするとエラー
product_frozen = Product(name=”Table”, price=50.0)
try:
product_frozen.price = 55.0
except TypeError as e:
print(“Frozen error:”, e) # TypeErrorが発生する
“`
主な設定オプション:
extra
: モデルに定義されていない追加のフィールドをどう扱うかを指定します。'ignore'
: 無視します(デフォルト値)。追加フィールドはモデルのインスタンスに含まれません。'forbid'
: 追加フィールドがあるとValidationError
を発生させます。厳密なデータ構造を強制したい場合に便利です。'allow'
: 追加フィールドを許可し、モデルインスタンスに含めます。動的なデータ構造に対応したい場合に便利です。
frozen
:True
に設定すると、モデルインスタンスのフィールドが不変(immutable)になります。インスタンス作成後に値を変更しようとするとTypeError
が発生します。populate_by_name
:True
に設定すると、エイリアスが定義されている場合でも、元のフィールド名(属性名)でデータを渡すことができるようになります。デフォルトはFalse
です。str_strip_whitespace
:True
に設定すると、文字列フィールドの先頭と末尾の空白を自動的に取り除きます。デフォルトはFalse
です。
これらの設定は、モデルの用途(APIリクエスト、DBスキーマなど)に応じて適切に構成することが重要です。
7.2 エイリアス (alias
, validation_alias
, serialization_alias
)
入力データのフィールド名が、Pythonコードで使いたい属性名と異なる場合があります(例: 外部APIのレスポンスでcamelCase
、Pythonコードではsnake_case
)。Pydanticでは、Field
関数を使ってエイリアスを定義することで、入力データとモデルの属性名をマッピングできます。
Pydantic V2では、エイリアスに関する設定がより詳細になりました。
alias
: バリデーション時(入力時)とシリアライズ時(出力時)の両方で使われるエイリアス。validation_alias
: バリデーション時(入力時)にのみ使われるエイリアス。入力の名前と属性の名前を明確に区別したい場合に便利。serialization_alias
: シリアライズ時(出力時)にのみ使われるエイリアス。
“`python
from pydantic import BaseModel, Field
class UserProfile(BaseModel):
# 入力で “emailAddress” を受け付け、属性名は email にする (両方でaliasを使用)
email: str = Field(…, alias=”emailAddress”)
# 入力で "zipCode" を受け付け、属性名は postal_code にする (validation_alias)
postal_code: str = Field(..., validation_alias="zipCode")
# 属性名は registration_date だが、出力時に "registeredAt" としたい (serialization_alias)
registration_date: str = Field(..., serialization_alias="registeredAt")
model_config = {'populate_by_name': True} # 属性名 (email, postal_code, registration_date) でも入力を受け付ける
data_aliased = {
“emailAddress”: “[email protected]”, # aliasで指定された名前
“zipCode”: “123-4567”, # validation_aliasで指定された名前
“registration_date”: “2023-10-27” # populate_by_name=True なので属性名でもOK
}
user_profile = UserProfile(**data_aliased)
print(user_profile) # インスタンスは属性名でアクセス
print(f”Email: {user_profile.email}”)
print(f”Postal Code: {user_profile.postal_code}”)
print(f”Registration Date: {user_profile.registration_date}”)
モデルを辞書に変換 (シリアライズ)
to_dict() は V1 のメソッド, model_dump() は V2 のメソッド
user_profile_dict = user_profile.model_dump()
print(“Serialized data:”, user_profile_dict) # serialization_alias が使われる (registration_date -> registeredAt)
“`
解説:
alias
は最もシンプルで、入力時と出力時で同じ別名を使いたい場合に便利です。validation_alias
は、入力データの名前とPythonコードの属性名を明確に分けたい場合に役立ちます。これにより、コード内部では統一された命名規則(例: snake_case)を使用できます。serialization_alias
は、モデルを外部にエクスポートする際に、属性名とは異なる名前を使いたい場合に便利です(例: APIレスポンスのキー名をcamelCase
にする)。model_config = {'populate_by_name': True}
を設定すると、エイリアスが定義されているフィールドでも、Python属性名(例:email
,postal_code
)を使って入力データを渡すことができるようになります。デフォルトでは、エイリアスが定義されている場合、エイリアス名でのみ入力が受け付けられます。
エイリアス機能は、外部システムとの連携において、データのキー名の違いを吸収するために非常に役立ちます。
7.3 デフォルト値
既に何度か触れていますが、フィールドにデフォルト値を設定することで、そのフィールドが入力データに含まれていなくてもエラーにならず、代わりに指定されたデフォルト値が使われます。
“`python
from pydantic import BaseModel
class Settings(BaseModel):
host: str = “localhost” # デフォルト値
port: int = 8000 # デフォルト値
debug: bool = False # デフォルト値
log_level: str # デフォルト値なし (必須フィールド)
log_level のみ指定
settings_partial = Settings(log_level=”INFO”)
print(settings_partial)
全て指定
settings_full = Settings(host=”192.168.1.1″, port=8080, debug=True, log_level=”DEBUG”)
print(settings_full)
log_level が欠けている (必須フィールド) -> エラー
try:
settings_invalid = Settings(host=”example.com”)
except ValidationError as e:
print(“Missing required field error:”, e.errors())
“`
デフォルト値は、省略可能な設定やオプションのデータフィールドに非常に便利です。デフォルト値が設定されていないフィールドは、必須フィールドとして扱われます。
デフォルト値が、リストや辞書のような可変オブジェクトである場合は注意が必要です。同じデフォルト値オブジェクトが複数のインスタンス間で共有されてしまう可能性があります。これを避けるには、default_factory
を使うか、Field
関数でデフォルト値を指定するのが安全です。
“`python
from pydantic import BaseModel, Field
from typing import List, Dict
class Item(BaseModel):
name: str
# tags: List[str] = [] # 可変オブジェクトをデフォルト値にするのは危険
tags: List[str] = Field(default_factory=list) # default_factory を使うか…
class Product(BaseModel):
name: str
price: float
# attributes: Dict[str, Any] = {} # 可変オブジェクトをデフォルト値にするのは危険
attributes: Dict = Field(default_factory=dict) # default_factory を使う
item1 = Item(name=”Laptop”)
item2 = Item(name=”Mouse”)
default_factory を使わないと、item1.tags と item2.tags が同じリストオブジェクトを指す可能性がある
item1.tags.append(“electronics”)
print(item1.tags)
print(item2.tags) # default_factory を使っているので item2.tags は空のまま
“`
default_factory
は、デフォルト値を生成するための引数なしの関数(またはその他の呼び出し可能オブジェクト)を受け取ります。モデルインスタンスが作成されるたびにこの関数が呼び出され、新しいデフォルト値が生成されるため、可変オブジェクトの共有を防げます。
8. Pydanticの活用例
Pydanticは、データバリデーションの目的だけでなく、様々な場面でその柔軟性と機能性を活かせます。
8.1 Webフレームワーク (FastAPIなど) でのリクエストボディ/クエリパラメータ/レスポンスモデル
Pydanticが最も広く使われているのは、FastAPIのようなモダンなWebフレームワークと組み合わせて使用する場合です。FastAPIはPydanticモデルを深く統合しており、以下のことが自動で行われます。
- リクエストデータのバリデーション: Pydanticモデルをエンドポイント関数の引数として指定するだけで、FastAPIはリクエストボディ(JSON)やクエリパラメータ、パスパラメータを自動的にパースし、指定されたPydanticモデルでバリデーションを行います。バリデーションエラーが発生した場合、FastAPIは自動的に422 Unprocessable Entityレスポンスと詳細なエラー情報を返します。
- レスポンスデータのシリアライズとバリデーション: Pydanticモデルをレスポンスモデルとして指定すると、エンドポイント関数が返したPythonオブジェクト(Pydanticモデルインスタンス、辞書など)が自動的にJSONに変換(シリアライズ)され、指定されたPydanticモデルでバリデーションされてからクライアントに返されます。
- 自動ドキュメント生成: Pydanticモデルの定義(フィールド名、型、制約、説明、例など)は、OpenAPI (Swagger) ドキュメントの生成に利用されます。
例 (FastAPI):
“`python
main.py (FastAPIアプリケーションの例)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import List, Optional
app = FastAPI()
リクエストボディ/レスポンスボディ用のPydanticモデル
class Item(BaseModel):
name: str = Field(…, example=”Awesome Item”)
price: float = Field(…, example=100.5)
is_offered: Optional[bool] = Field(None, example=True)
APIエンドポイントの定義
@app.post(“/items/”)
async def create_item(item: Item): # Pydanticモデルを引数に指定
# item オブジェクトは既にバリデーション済み
print(f”Received item: {item.model_dump_json()}”) # PydanticモデルをJSON文字列に変換 (v2)
return item # Pydanticモデルを返すと、FastAPIがJSONにシリアライズしてバリデーション
クエリパラメータ/パスパラメータの例 (Pydanticモデルも使えるが、Field関数で型ヒントとバリデーションを指定するのが一般的)
from typing import Annotated
from fastapi import Query, Path
@app.get(“/items/{item_id}”)
async def read_item(
item_id: Annotated[int, Path(title=”The ID of the item to get”, ge=1)], # パスパラメータ + バリデーション
q: Annotated[Optional[str], Query(alias=”item-query”, min_length=3, max_length=50)] = None # クエリパラメータ + エイリアス + バリデーション
):
return {“item_id”: item_id, “q”: q}
“`
FastAPIを使うと、Pydanticによるバリデーションとシリアライズの恩恵を最大限に受けることができます。FlaskやDjangoなどの他のフレームワークでも、Pydanticをバリデーション層として組み込むことは可能です。
8.2 設定ファイルの読み込み
Pydanticは、環境変数や設定ファイル(JSON, YAMLなど)からアプリケーションの設定を読み込み、バリデーションするためにも非常に適しています。Pydantic V2では、pydantic_settings
という別のライブラリ(以前はPydantic本体の一部)を使うのが標準的な方法です。
“`python
.env ファイルの例
DATABASE_URL=postgresql://user:password@host:port/dbname
API_KEY=abcdef123456
DEBUG=true
settings.py
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional
pydantic_settings をインストール: pip install pydantic-settings
class AppSettings(BaseSettings):
database_url: str
api_key: str = Field(…, min_length=10) # バリデーションも適用可能
debug: bool = False
log_level: str = “INFO”
timeout_seconds: int = 30
model_config = SettingsConfigDict(
env_file=".env", # .env ファイルから読み込む
env_file_encoding='utf-8',
case_sensitive=False, # 環境変数名を大文字小文字区別しない
# env_prefix="APP_" # 環境変数にプレフィックスをつける場合 (例: APP_DATABASE_URL)
)
環境変数や .env ファイルから設定を読み込み、バリデーション
settings = AppSettings()
print(f”Database URL: {settings.database_url}”)
print(f”API Key: {settings.api_key}”) # API_KEY 環境変数から読み込まれる
print(f”Debug: {settings.debug}”) # DEBUG 環境変数から読み込まれる
print(f”Log Level: {settings.log_level}”) # .envに無ければデフォルト値
print(f”Timeout: {settings.timeout_seconds}”) # .envに無ければデフォルト値
“`
BaseSettings
はBaseModel
を継承しており、SettingsConfigDict
を使って環境変数やファイルからの読み込み方法を設定します。これにより、アプリケーションの設定管理が型安全かつバリデーション付きで行えるようになります。
8.3 APIレスポンスの整形
Pydanticモデルを使って、APIのレスポンスデータを整形し、必要なフィールドだけを含めたり、フィールド名を変更したりできます。入力データをモデルにパースし、そのモデルインスタンスを返すだけで、Pydanticが定義に基づいてデータをシリアライズします。
“`python
from pydantic import BaseModel, Field
from typing import Optional
class InternalUser(BaseModel):
# 内部データ構造 (DBなどから取得)
_id: str
username: str
hashed_password: str
full_name: Optional[str] = None
is_active: bool = True
created_at: str # 例として文字列
class PublicUser(BaseModel):
# 外部APIレスポンス用の構造
user_id: str = Field(…, alias=”_id”) # _id を user_id として公開
username: str
full_name: Optional[str] = None
is_active: bool = True
# 外部に公開したくないフィールド (hashed_password, created_at など) はPublicUserに含めない
model_config = {'populate_by_name': True} # Python属性名でも入力受け付け
内部データ (DBから取得したようなデータ)
internal_data = {
“_id”: “user-abc”,
“username”: “johndoe”,
“hashed_password”: “very-secret-hash”,
“full_name”: “John Doe”,
“is_active”: True,
“created_at”: “2023-01-01T10:00:00Z”
}
内部データをPublicUserモデルでパース -> バリデーションと整形が同時に行われる
不要なフィールド (_id以外の内部フィールド) は自動的に無視される (extra=’ignore’がデフォルト)
_id は alias=”user_id” により user_id として扱われる
public_user_instance = PublicUser(**internal_data)
PublicUserインスタンスを辞書に変換 (APIレスポンス用)
model_dump() は serialization_alias を考慮する
public_user_response = public_user_instance.model_dump(by_alias=True) # by_alias=True で alias を使用
print(“Internal Data:”, internal_data)
print(“Public User Instance:”, public_user_instance)
print(“Public User Response (for API):”, public_user_response)
“`
この例では、内部的なInternalUser
モデルで表現されるデータ構造から、外部に公開するためのPublicUser
モデルに変換しています。PublicUser
モデルは、必要なフィールドのみを持ち、_id
フィールドをuser_id
というエイリアスで公開しています。このように、Pydanticモデルはデータの入出力形式を定義するスキーマとして機能し、変換や整形を効率的に行えます。
8.4 データ変換 (ORMなどからのデータ変換)
データベースから取得したデータ(ORMオブジェクトなど)をAPIレスポンスや他の形式に変換する際にもPydanticが役立ちます。ORMオブジェクトをPydanticモデルに渡すことで、型変換や必要なフィールドの抽出を自動で行えます。
多くのORM(SQLAlchemyなど)オブジェクトは、属性アクセスでデータにアクセスできます。Pydanticは、属性アクセス可能なオブジェクト(ORMオブジェクトなど)を辞書のように扱ってパースすることができます。
“`python
from pydantic import BaseModel
ORMモデルを模倣したダミークラス
class DBSession:
def query(self, model):
return DummyQuery()
class DummyQuery:
def get(self, item_id):
# DBから取得したデータ(ORMオブジェクトを模倣)
return DummyORMItem(id=item_id, name=”Widget”, db_price=15.50, db_description=”A useful widget from DB”)
class DummyORMItem:
def init(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
# Pydanticが属性アクセスでデータを取得できるように __getitem__ を定義する場合がある
# SQLAlchemyモデルなどはデフォルトでこれができることが多い
def __getitem__(self, item):
return getattr(self, item)
Pydanticモデル (ORMデータをAPIレスポンス用に変換)
class APIItem(BaseModel):
item_id: int = Field(…, alias=”id”) # ORMの id フィールドを item_id にマッピング
item_name: str = Field(…, alias=”name”) # ORMの name フィールドを item_name にマッピング
price: float = Field(…, alias=”db_price”) # ORMの db_price フィールドを price にマッピング
description: str = Field(…, alias=”db_description”, serialization_alias=”shortDescription”) # ORMの db_description を description にマッピングし、出力時は shortDescription
model_config = {'from_attributes': True} # V2: ORMオブジェクトなどの属性からデータを読み込む設定
db = DBSession()
orm_item = db.query(DummyORMItem).get(1)
ORMオブジェクトをPydanticモデルにパース
from_attributes=True (v2) または orm_mode=True (v1) が必要
api_item = APIItem.model_validate(orm_item) # V2での推奨されるパース方法
print(“ORM Item (attributes):”, orm_item.dict)
print(“API Item (Pydantic model):”, api_item)
APIレスポンス用にシリアライズ
api_response_data = api_item.model_dump(by_alias=True)
print(“API Response Data:”, api_response_data)
“`
解説:
- Pydantic V2では、
model_config = {'from_attributes': True}
を設定することで、BaseModel.model_validate()
メソッドにORMオブジェクトなどの属性アクセス可能なオブジェクトを渡せるようになります。Pydanticはオブジェクトの属性を読み取り、それを辞書のように扱ってモデルのパースを行います。 - V1では、
Config
クラスにorm_mode = True
を設定し、BaseModel.from_orm()
メソッドを使用しました。 - エイリアスを使うことで、ORMのフィールド名とAPIレスポンスのフィールド名を柔軟にマッピングできます。
- この方法により、DBスキーマとAPIスキーマを分離しつつ、効率的なデータ変換パイプラインを構築できます。
9. Pydantic V1 vs V2
Pydanticは活発に開発されており、2023年6月にPydantic V2がリリースされました。V1からの主な変更点と、移行時のポイントを簡単に説明します。
9.1 主な変更点
- パフォーマンス向上: Rust製の
pydantic-core
の導入により、パースとバリデーションの速度が大幅に向上しました。 - 設定方法の変更:
Config
内部クラスが非推奨となり、model_config
クラス属性(辞書またはConfigDict
)に置き換わりました。 - バリデーターデコレーターの変更:
@validator
と@root_validator
が非推奨となり、@field_validator
と@model_validator
に置き換わりました。V2のバリデーターはより柔軟で、mode
を指定できるようになりました。 - エイリアス設定の強化:
alias
に加えて、validation_alias
とserialization_alias
が追加され、入出力で異なるエイリアスを使い分けられるようになりました。 - パース方法の変更:
parse_obj
,parse_raw
,from_orm
などのクラスメソッドが非推奨となり、model_validate
,model_validate_json
,model_construct
などのメソッドに置き換わりました。ORMモードはorm_mode=True
からmodel_config={'from_attributes': True}
に変更され、メソッドもfrom_orm
からmodel_validate
に統合されました。 - データ出力方法の変更:
dict()
,json()
メソッドが非推奨となり、それぞれmodel_dump()
とmodel_dump_json()
に置き換わりました。これらのメソッドもオプションが強化されています。 - フィールド定義の強化:
Field
関数が多くのバリデーション制約を受け付けるようになり、pydantic.types
にあった一部の型(constr
,conint
など)が不要になりました。 - エラー報告の改善: バリデーションエラーの情報がより構造化され、エラーメッセージも改善されました。
9.2 移行のヒント
- まずはV2をインストールし、コードを実行してみます。V2は多くのV1コードに対して後方互換性がありますが、非推奨になった機能を使っている場合は警告が表示されます。
- 表示される警告メッセージを確認し、非推奨になった機能をV2の新しい方法に置き換えていきます。特に、
Config
->model_config
,@validator
/@root_validator
->@field_validator
/@model_validator
,parse_obj
/from_orm
->model_validate
,dict()
/json()
->model_dump()
/model_dump_json()
の変更が中心になります。 pydantic-migrate
という公式ツールを利用すると、V1コードをV2に自動変換する手助けをしてくれます。ただし、複雑なコードやカスタムロジックを含む場合は手動での修正が必要になる場合があります。- 新しい
Field
関数や@model_validator
など、V2で強化された機能を積極的に活用することで、より洗練されたコードになります。 - FastAPIなどのPydanticを使用しているライブラリのバージョンも、V2対応のものにアップデートすることをお勧めします。
V2への移行は、パフォーマンス向上や新しい機能の恩恵を受けるために価値があります。ドキュメントを参照しながら、計画的に移行を進めましょう。
10. Pydanticの高度な機能 (軽く触れる)
入門記事としては深入りしませんが、Pydanticには他にも多くの強力な機能があります。
- Fieldを用いたより詳細な設定:
Field
関数は、デフォルト値、エイリアス、制約だけでなく、title
,description
,examples
といったメタデータも指定できます。これらは生成されるJSON Schemaやドキュメントに反映されます。 - ジェネリックモデル: Pythonの
typing.Generic
と組み合わせて、汎用的なデータ構造(例: ページネーションされたリスト)を表現するジェネリックなPydanticモデルを定義できます。 - JSON Schemaの生成: Pydanticモデルから、そのモデルが表現するデータ構造を定義するJSON Schemaを自動生成できます。これは、APIドキュメントの生成や、他のシステムとのデータ形式の共有に非常に便利です。
MyModel.model_json_schema()
(v2) またはMyModel.schema()
(v1) メソッドを使用します。 - データのエクスポート:
model_dump()
やmodel_dump_json()
メソッドには、インクルード/エクスクルードするフィールドを指定したり、エイリアスを使用するかどうかを指定したりするなど、様々なオプションがあります。 - ORMモードの進化: V2の
from_attributes=True
(旧orm_mode=True
) は、単なるORMオブジェクトだけでなく、任意の属性アクセス可能なオブジェクトからのパースをサポートします。
これらの機能は、特定の高度なユースケースで非常に役立ちますが、まずは基本的な使い方をマスターすることから始めましょう。
11. まとめ
この記事では、PythonのPydanticライブラリを使ったデータバリデーションの基本から応用までを詳細に解説しました。
Pydanticは、Pythonの型ヒントを活用することで、以下のメリットを提供します。
- シンプルなモデル定義: クラス属性に型ヒントを指定するだけで、データ構造とバリデーションルールを定義できます。
- 自動バリデーションと型変換: 入力データをモデルに渡すだけで、定義に基づいた厳格なバリデーションと賢い型変換が自動で行われます。
- 明確なエラー報告: バリデーションに失敗した場合、エラー箇所と内容が詳細に報告されるため、デバッグやエラーハンドリングが容易です。
- コード量の削減: 手作業でのバリデーションコード記述が不要になり、ボイラープレートを大幅に削減できます。
- 型安全性の向上: 型ヒントによりコードの意図が明確になり、静的解析ツールやIDEの恩恵を受けやすくなります。
- 高いパフォーマンス: Rustによるコア実装により、大規模なデータでも高速に処理できます。
- 豊富な機能: ネストされたモデル、カスタムバリデーション、設定管理、JSON Schema生成など、多様な機能で様々なユースケースに対応します。
Web API開発におけるリクエスト/レスポンスのバリデーション、設定ファイルの読み込み、外部データソース(データベース、ファイル、外部API)からのデータのパースと変換など、Pydanticは様々な場面でデータ処理の信頼性と効率を向上させます。
特にFastAPIとの組み合わせは非常に強力で、両者を活用することで型安全で自己ドキュメント化された高速なWeb APIを効率的に開発できます。
Pydanticは、データを取り扱う全てのPython開発者にとって、非常に価値のあるツールです。この記事で学んだ内容を参考に、ぜひあなたのプロジェクトでPydanticを活用し、より堅牢で保守しやすいコードを書いていきましょう。