はじめに
前回の記事では、FastAPIを使うと型定義だけでバリデーションが自動になると紹介しました。
class UserCreate(BaseModel):
name: str
age: int
でも、「なぜこれだけでバリデーションが動くの?」という疑問は残りますよね。
今回はその裏側を担っている Pydantic に焦点を当てて、実務でよく使うパターンを一通り解説します。
1. Pydanticとは?
Pydantic は、Pythonの型ヒントを使ってデータのバリデーションと変換を行うライブラリです。FastAPIはPydanticを内部で使っており、FastAPIを入れると自動でインストールされます。
pip install pydantic # FastAPIと一緒に入るので通常は不要
一番シンプルな使い方はこうです:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
# 正しいデータ → そのまま使える
user = User(name="Alice", age=30)
print(user.name) # Alice
# 間違ったデータ → ValidationError が発生
user = User(name="Alice", age="三十") # ageに文字列を渡した
# → pydantic_core._pydantic_core.ValidationError: ...
FastAPIでは、このバリデーションがリクエストを受け取った瞬間に自動で走ります。エラーがあれば 422 Unprocessable Entity を返してくれます。
2. 基本の型ヒント
Pydanticで使える主な型を整理します。
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class Item(BaseModel):
name: str # 文字列(必須)
price: float # 浮動小数点(必須)
stock: int # 整数(必須)
is_available: bool # 真偽値(必須)
tags: List[str] # 文字列のリスト(必須)
description: Optional[str] = None # 文字列 or None(任意、デフォルトNone)
created_at: datetime # 日時(必須)
💡
Optional[str]とは?
strまたはNoneのどちらでも受け付ける、という意味です。
Optional[str] = Noneと書くと「省略可能なフィールド」になります。
3. Fieldでバリデーションを細かく制御する
型だけでは「文字列であること」しか保証できません。
「文字列で、かつ1〜50文字以内」のような詳細な制約には Field を使います。
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(
min_length=1, # 最小文字数
max_length=50, # 最大文字数
description="ユーザー名" # ドキュメント用の説明
)
age: int = Field(
ge=0, # greater than or equal(0以上)
le=120, # less than or equal(120以下)
description="年齢"
)
email: str = Field(
pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$", # 正規表現でメール形式を確認
description="メールアドレス"
)
score: float = Field(
default=0.0, # デフォルト値
ge=0.0,
le=100.0,
description="スコア(0.0〜100.0)"
)
Fieldの主要なオプション一覧
| オプション | 意味 | 対象 |
|---|---|---|
default |
デフォルト値 | 全型 |
min_length |
最小文字数 | str |
max_length |
最大文字数 | str |
pattern |
正規表現パターン | str |
ge |
以上(≥) | int, float |
le |
以下(≤) | int, float |
gt |
より大きい(>) | int, float |
lt |
より小さい(<) | int, float |
description |
Swagger UIに表示される説明 | 全型 |
examples |
Swagger UIに表示されるサンプル値 | 全型 |
4. ネストしたモデル(モデルの中にモデル)
実務では、データが入れ子構造になることがよくあります。
from pydantic import BaseModel, Field
from typing import List
class Address(BaseModel):
prefecture: str = Field(description="都道府県")
city: str = Field(description="市区町村")
zipcode: str = Field(pattern=r"^\d{3}-\d{4}$", description="郵便番号(例: 100-0001)")
class UserCreate(BaseModel):
name: str
age: int
address: Address # モデルをそのまま型として使える
tags: List[str] = [] # リストのデフォルトは []
リクエストのJSONはこうなります:
{
"name": "Alice",
"age": 30,
"address": {
"prefecture": "東京都",
"city": "千代田区",
"zipcode": "100-0001"
},
"tags": ["admin", "user"]
}
Pydanticはネストした Address の中のバリデーションもすべて自動で走らせます。
zipcode が "100-001" のようなフォーマット違いでもきちんとエラーになります。
5. よくあるエラーと原因
エラー例1:型の不一致
class Item(BaseModel):
price: float
Item(price="高い") # 文字列を渡した
ValidationError: 1 validation error for Item
price
Input should be a valid number [type=float_parsing, ...]
エラー例2:必須フィールドの欠落
class Item(BaseModel):
name: str
price: float
Item(name="りんご") # price を渡し忘れた
ValidationError: 1 validation error for Item
price
Field required [type=missing, ...]
エラー例3:Fieldの制約違反
class UserCreate(BaseModel):
age: int = Field(ge=0, le=120)
UserCreate(age=200) # 120を超えた
ValidationError: 1 validation error for UserCreate
age
Input should be less than or equal to 120 [type=less_than_equal, ...]
FastAPIではこれらのエラーが自動で整形されて、クライアントに 422 レスポンスとして返ります。自分でtry/exceptを書く必要はありません。
6. FastAPIでの実践例
ここまでの内容をFastAPIに組み込んでみます。
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional, List
app = FastAPI()
class Address(BaseModel):
prefecture: str
city: str
zipcode: str = Field(pattern=r"^\d{3}-\d{4}$")
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=50, description="ユーザー名")
age: int = Field(ge=0, le=120, description="年齢")
email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$", description="メール")
address: Address
tags: List[str] = []
bio: Optional[str] = None
class UserResponse(BaseModel):
id: int
name: str
age: int
email: str
@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
# 実際はDBに保存する処理が入る
# ここではダミーのIDを返す
return UserResponse(id=1, name=user.name, age=user.age, email=user.email)
💡
response_modelとは?
レスポンスの形を定義するモデルです。
UserCreateに含まれるaddressやtagsを返したくないとき、
UserResponseで返すフィールドだけを定義すれば自動でフィルタリングされます。
パスワードなど、入力は受けるが返してはいけないフィールドの隠蔽に便利です。
7. まとめ
Pydanticを使うと:
-
str、int、floatなどの基本型で型チェックが自動になる -
Fieldを使えば文字数制限・数値範囲・正規表現などの詳細な制約を追加できる - モデルを入れ子にすることで複雑なデータ構造にも対応できる
- バリデーションエラーは FastAPI が自動で
422レスポンスにしてくれる -
response_modelでレスポンスのフィールドを制御できる
次回は パスパラメータ・クエリパラメータ・リクエストボディの使い分け を解説します。「URLの {id} と ?page=2 は何が違うの?」という疑問をコードと図で整理します。
参考
この記事は学習日記として書いています。間違いや補足があればコメントいただけると嬉しいです!