Python Pydantic入門:データバリデーションを簡単に


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の場合、ageis_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モジュールからUnionOptionalを使用します。

  • 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('フィールド名')デコレーターは、指定されたフィールドの値を引数として受け取り、バリデーションを行います。バリデーションが成功した場合は、その値を返す必要があります。失敗した場合はValueErrorTypeErrorなどの例外を発生させます。

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つのモードがあります。

  1. Before Validation (mode='before'): 入力データ(パース前の辞書など)を受け取り、それを変換してからPydanticの標準バリデーションに渡したい場合に使用します。
  2. 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_dateself.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: Companyaddress: 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に無ければデフォルト値

“`

BaseSettingsBaseModelを継承しており、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_aliasserialization_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を活用し、より堅牢で保守しやすいコードを書いていきましょう。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール