0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPI + Pydantic — リクエスト/レスポンスの型定義を整理した

0
Posted at

はじめに

前の記事で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=trueboolTrueに変換される。

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で統一できるのはシンプルで覚えやすかった。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?