はじめに
前の記事でFastAPIの基本を触った。
Pydanticについては「バリデーションができる」くらいの理解で終わっていたので、もう少し掘り下げた。型定義の幅が思ったより広くて、PHPのFormRequestに似た役割をPydanticが担いつつ、さらにレスポンスの整形まで一貫してやれるのが地味に気持ちいい。
型安全が標準で付いてくるという感覚は、PHPでは追加ライブラリがないとなかなか得られなかったもの。
Pydanticとは
FastAPIが採用しているデータバリデーション・型変換ライブラリ。
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str
# 正常な入力
user = User(name="田中", age=28, email="tanaka@example.com")
print(user.name) # 田中
print(user.model_dump())
# {'name': '田中', 'age': 28, 'email': 'tanaka@example.com'}
# 型が違う場合は自動変換を試みる
user2 = User(name="鈴木", age="35", email="suzuki@example.com")
print(user2.age) # 35(文字列→intに変換された)
print(type(user2.age)) # <class 'int'>
# 変換できない場合はValidationError
user3 = User(name="佐藤", age="abc", email="sato@example.com")
# ValidationError: age → Input should be a valid integer
PHPのFormRequestはHTTPリクエストに特化しているが、PydanticはHTTPリクエスト以外でも使えるのが違い。関数の引数や設定ファイルの読み込みにも使える汎用的なライブラリ。
Fieldで詳細なバリデーション
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50, description="ユーザー名")
age: int = Field(..., ge=0, le=150, description="年齢")
password: str = Field(..., min_length=8, description="パスワード")
score: float = Field(0.0, ge=0.0, le=100.0, description="スコア")
tags: list[str] = Field(default_factory=list, description="タグ一覧")
Field()の第一引数は:
-
...(Ellipsis)→ 必須フィールド - 値を指定 → デフォルト値(省略可能)
主なバリデーションオプション:
| オプション | 意味 | PHP相当 |
|---|---|---|
min_length |
最小文字数 | min:N |
max_length |
最大文字数 | max:N |
ge |
以上(>=) | min:N |
le |
以下(<=) | max:N |
gt |
より大きい(>) | gt:N |
lt |
より小さい(<) | lt:N |
pattern |
正規表現 | regex:パターン |
description |
フィールドの説明 | SwaggerのUIに表示 |
バリデーター — カスタムバリデーション
Fieldで対応できない複雑なバリデーションは@field_validatorで書く。
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
password: str = Field(..., min_length=8)
password_confirmation: str
@field_validator("name")
@classmethod
def name_must_be_alphanumeric(cls, v: str) -> str:
if not v.replace("_", "").isalnum():
raise ValueError("名前は英数字とアンダースコアのみ使用できます")
return v
@field_validator("password")
@classmethod
def password_must_contain_digit(cls, v: str) -> str:
if not any(c.isdigit() for c in v):
raise ValueError("パスワードには数字を含めてください")
return v
複数フィールドをまたぐバリデーション
from pydantic import BaseModel, model_validator
class UserCreate(BaseModel):
password: str
password_confirmation: str
@model_validator(mode="after")
def passwords_match(self) -> "UserCreate":
if self.password != self.password_confirmation:
raise ValueError("パスワードが一致しません")
return self
@model_validatorはモデル全体にアクセスできる。PHPのFormRequestでwithValidator()を使って複数フィールドを比較するのと同じことが@model_validatorでできる。
ネストしたモデル
from pydantic import BaseModel
class Address(BaseModel):
postal_code: str
prefecture: str
city: str
street: str
class UserCreate(BaseModel):
name: str
email: str
address: Address # ネストしたモデル
# 使う側
user = UserCreate(
name="田中",
email="tanaka@example.com",
address={
"postal_code": "100-0001",
"prefecture": "東京都",
"city": "千代田区",
"street": "千代田1-1",
}
)
print(user.address.prefecture) # 東京都
print(user.model_dump())
# {
# 'name': '田中',
# 'email': 'tanaka@example.com',
# 'address': {
# 'postal_code': '100-0001',
# 'prefecture': '東京都',
# 'city': '千代田区',
# 'street': '千代田1-1'
# }
# }
JSONをネストした辞書として受け取って、型付きオブジェクトとしてアクセスできる。PHPだと多次元連想配列で$user['address']['prefecture']と書くところが、Pydanticだとuser.address.prefectureとドット記法でアクセスできる。
リストのネスト
from pydantic import BaseModel
class Tag(BaseModel):
id: int
name: str
class Article(BaseModel):
title: str
body: str
tags: list[Tag] # Tagのリスト
article = Article(
title="Python入門",
body="Pythonの基礎を学ぶ記事",
tags=[
{"id": 1, "name": "Python"},
{"id": 2, "name": "入門"},
]
)
for tag in article.tags:
print(tag.name) # Python, 入門
辞書のリストが自動的にTagオブジェクトのリストに変換される。
リクエストとレスポンスでモデルを分ける
これがFastAPI + Pydanticでよく使われるパターン。
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
from fastapi import FastAPI
app = FastAPI()
# リクエスト用モデル(入力)
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
# レスポンス用モデル(出力)
class UserResponse(BaseModel):
id: int
name: str
email: str
created_at: datetime
# passwordは含めない
# 更新用モデル(全フィールドが省略可能)
class UserUpdate(BaseModel):
name: str | None = None
email: str | None = None
@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
# DBに保存(省略)
saved = {
"id": 1,
"name": user.name,
"email": user.email,
"password": user.password, # DBには保存
"created_at": datetime.now(),
}
return saved
# response_modelがUserResponseなのでpasswordは自動除外
このパターンの整理:
-
UserCreate— POST時の入力。passwordあり -
UserResponse— レスポンス。passwordなし -
UserUpdate— PATCH時の入力。全フィールド省略可能
PHPのFormRequestとAPIResourceを組み合わせたパターンに相当する。Pydanticでは入力と出力を別モデルとして定義することで責務が明確になる。
model_config — モデルの設定
from pydantic import BaseModel, ConfigDict
class UserResponse(BaseModel):
model_config = ConfigDict(
from_attributes=True, # ORMオブジェクトから直接生成できる
populate_by_name=True, # フィールド名でも代入可能
)
id: int
name: str
email: str
from_attributes=Trueを設定すると、辞書だけでなくORMのオブジェクトをそのまま渡せる。SQLAlchemyと組み合わせるときに必須になる設定。
# SQLAlchemyのORMオブジェクトをそのまま渡せる
db_user = db.query(UserModel).first()
response = UserResponse.model_validate(db_user)
Annotatedで型とバリデーションをまとめる
from typing import Annotated
from pydantic import BaseModel, Field
# よく使う型定義を再利用可能にする
PositiveInt = Annotated[int, Field(gt=0)]
ShortStr = Annotated[str, Field(min_length=1, max_length=50)]
EmailAddress = Annotated[str, Field(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
class UserCreate(BaseModel):
name: ShortStr
age: PositiveInt
email: EmailAddress
同じバリデーション条件を複数のモデルで使うとき、Annotatedでまとめると重複が減る。
バリデーションエラーのハンドリング
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError
app = FastAPI()
# FastAPIが自動で422を返すのをカスタマイズしたい場合
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
):
errors = []
for error in exc.errors():
errors.append({
"field": " → ".join(str(x) for x in error["loc"][1:]),
"message": error["msg"],
"type": error["type"],
})
return JSONResponse(
status_code=422,
content={"errors": errors},
)
デフォルトのエラーレスポンスをカスタマイズしたい場合に使う。フロントエンドとのインターフェースに合わせてレスポンス形式を変えるときに触ることになる。
設定管理にPydanticを使う
FastAPIの外でも使えるPydanticの便利な使い方。
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "MyAPI"
debug: bool = False
database_url:str
secret_key: str
allowed_origins: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
# シングルトンとして使う
settings = Settings()
print(settings.database_url)
# .env
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=super-secret-key
DEBUG=true
pydantic-settingsを使うと.envファイルや環境変数の値を型付きで読み込める。PHPのconfig()ヘルパーやdotenvに相当する。型変換も自動でやってくれるのでDEBUG=trueがboolのTrueに変換される。
pip install pydantic-settings
PHPのFormRequest/APIResourceとの比較
| 機能 | PHP(Laravel) | Python(Pydantic) |
|---|---|---|
| リクエストバリデーション | FormRequest |
BaseModel + Field
|
| カスタムバリデーション | withValidator() |
@field_validator |
| 複数フィールド比較 | withValidator() |
@model_validator |
| レスポンス整形 | APIResource |
response_model |
| 設定ファイル管理 |
.env + config()
|
pydantic-settings |
| ORMの型変換 | Eloquentの自動変換 | from_attributes=True |
まとめ
Pydanticを使いこなすと型の恩恵をかなり広く受けられる。
- リクエストの入力バリデーション
- レスポンスの出力フィルタリング
- 設定ファイルの型付き読み込み
- ORMオブジェクトの変換
この4つが一つのライブラリで一貫して書けるのは設計としてきれいだと思う。PHPでFormRequestとAPIResourceと設定ファイルがそれぞれ別の仕組みで動いているのと比べると、Pydanticで統一できるのはシンプルで覚えやすかった。