概要(バリデーション=「変な値は中に入れない門番」)
バリデーションは、
「外から送られてきた値が、想定どおりかチェックして、
おかしければ入口で止める」
仕組みです。
Web フレームワーク(FastAPI など)では、
リクエストの中身(URL、クエリ、JSON ボディなど)は全部「外部からの入力」です。
ここをちゃんとチェックしないと、
数字だと思っていたら文字列が来る
必須の項目が抜けている
日付としてありえない値が来る(13月32日など)
といった“変な値”が中まで入り込み、
内部処理で例外が出たり、バグやセキュリティ問題の原因になったりします。
バリデーションは、「アプリの入り口に立つ門番」です。
正しい値だけ通し、変な値はエラーレスポンスにして返します。
どこで何をバリデーションするのか(全体像をイメージする)
URL・パスパラメータ・クエリ・ボディ、それぞれにチェックがある
FastAPI を例にすると、入力はざっくり次のような場所に現れます。
エンドポイントのパス(/users/{user_id} の user_id)
クエリパラメータ(?limit=10&keyword=apple)
リクエストボディ(POST/PUT などの JSON)
それぞれに「型」「必須/任意」「範囲」「フォーマット」などの制約を考えます。
基本的な考え方はこうです。
パスパラメータやクエリは「型ベースのチェック」が主役
リクエストボディは「Pydantic モデル+カスタムルール」でしっかり設計
FastAPI+Pydantic を使うと、
型ヒントを書く → だいたいのバリデーションは自動
という状態までかなり持っていけます。
型ヒントによる自動バリデーション(FastAPI の基本)
パスパラメータの型チェック
まず、パスパラメータでのバリデーション例を見てみます。
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
Pythonポイントは user_id: int です。
FastAPI はここを見て、
user_id は整数であるべき
文字列が来たらエラー(400 Bad Request)
というルールを自動的に適用します。
/users/10 → OK(user_id=10)/users/abc → 400 エラー(int に変換できない)
つまり、「型ヒント自体がバリデーションルール」になっています。
文字列なら str、浮動小数なら float、真偽値なら bool。
これをパスパラメータに付けるだけで、基本的なチェックが手に入ります。
クエリパラメータの型・必須/任意・デフォルト
クエリパラメータでも同じです。
@app.get("/search")
def search(keyword: str, limit: int = 10):
return {"keyword": keyword, "limit": limit}
Pythonここでは、
keyword: str
必須。指定されなければエラー。
limit: int = 10
整数で、指定がなければ 10。
/search?keyword=apple → OK(limit=10)/search?keyword=apple&limit=5 → OK(limit=5)/search?keyword=apple&limit=abc → 400 エラー(int 変換不可)
型ヒントとデフォルト値が、「必須/任意」と「型チェック」を一気に表現してくれます。
Pydantic モデルによるリクエストボディのバリデーション(ここが本丸)
モデルで「JSON の形」を定義してチェックする
POST / PUT で受け取る JSON は、だいたい複雑になりがちです。
ここで効いてくるのが Pydantic モデルです。
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import Optional
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: EmailStr
age: Optional[int] = None
is_active: bool = True
@app.post("/users")
def create_user(user: UserCreate):
return {"message": "created", "user": user}
Pythonこの例で、UserCreate が「リクエストボディのバリデーションルールそのもの」です。
username: str
必須の文字列。ないと 422 エラー。
email: EmailStr
必須。メールアドレスとして有効な文字列でなければ 422 エラー。
age: Optional[int] = None
任意。指定されなければ None。指定されれば int に変換される。
文字列や「abc」などはエラー。
is_active: bool = True
任意。指定がなければ True。
クライアントが JSON を送ると、FastAPI は内部でこうします。
JSON を読み取る
UserCreate に渡して Pydantic がバリデーションする
問題なければ user 引数として関数に渡す
問題があれば 422 (Unprocessable Entity) でエラー内容を返す
この結果、「関数の中では正しい形の user だけが来る」前提でコードを書けます。
if 文で「このキーあるかな?」「型合ってるかな?」といちいちチェックしなくてよくなります。
値の範囲・長さ・パターンのバリデーション(型だけでは足りない部分)
数値の範囲チェック(例:0 ≤ age ≤ 120)
型だけでは、「年齢がマイナス」みたいな不正は防げません。
ここで使うのが Field です。
from pydantic import BaseModel, Field
class User(BaseModel):
age: int = Field(..., ge=0, le=120)
PythonField(..., ge=0, le=120) の意味は、
必須(…)
0 以上(ge: greater or equal)
120 以下(le: less or equal)
です。
age=-1 → エラーage=200 → エラーage=30 → OK
FastAPI+Pydantic は、
JSON → User モデル変換の途中で、この範囲チェックも行います。
文字列の長さ・正規表現
文字列の長さやパターンも同様にチェックできます。
class User(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
postal_code: str = Field(..., regex=r"^\d{3}-\d{4}$")
Pythonusername
3文字以上 20文字以下。"" や 2 文字はエラー。
postal_code
「123-4567」のような 3桁-4桁形式だけ許可。
「こういう文字列じゃなきゃダメ」という制約をモデル側に書いておくことで、
エンドポイント側は「すでに綺麗に整えられたデータ」だけ扱えば良くなります。
カスタムバリデーション(「業務的なルール」をモデルに埋める)
単純な if 文だけでは表現しにくいルール
実務だと、次のような「業務ルール」も出てきます。
終了日時は開始日時より後でなければならない
大人の年齢なら 18 歳以上
日本在住なら郵便番号必須、海外在住なら不要
こういう「複数フィールドにまたがるチェック」は、
Pydantic の validator を使うとモデルに寄せられます。
例:開始日より終了日が後かどうか
from datetime import date
from pydantic import BaseModel, field_validator
class Event(BaseModel):
title: str
start_date: date
end_date: date
@field_validator("end_date")
@classmethod
def check_dates(cls, end_date: date, info):
start_date = info.data.get("start_date")
if start_date and end_date < start_date:
raise ValueError("end_date must be after start_date")
return end_date
Python(Pydantic v2 の書き方の一例です。v1 なら @validator を使います。)
このモデルを使って、
Event(title="Sample", start_date="2025-01-10", end_date="2025-01-05")
Pythonとすると、end_date < start_date なのでバリデーションエラーになります。
ポイントは、
「業務ルールそのもの」をモデルの中に閉じ込められる
ということです。
FastAPI 側の関数では、
@app.post("/events")
def create_event(event: Event):
...
Pythonと書くだけ。
エンドポイントは「正しい Event だけ来る」前提で書いてよく、
「日付の前後大丈夫かな?」といったチェックは Event モデルに任せられます。
レスポンス側のバリデーション(出すものにも責任を持つ)
response_model で「返してよい形」を決める
バリデーションは「入力だけ」ではありません。
「出力(レスポンス)」にも使えます。
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
id: int
name: str
fake_db = [
Item(id=1, name="Apple"),
Item(id=2, name="Banana"),
]
@app.get("/items", response_model=List[Item])
def list_items():
return fake_db
Pythonここでは、
返すべきレスポンスは「Item のリスト」であるべき
という契約になっています。
FastAPI は、関数の戻り値がこの形から逸れていないかチェックし、
余計なフィールドがあれば落としたり、
場合によってはエラーにしたりします。
「内部ではいろいろな情報を持っていても、外にはこれだけ出す」
というのを response_model で表現し、
それを Pydantic が保証してくれるイメージです。
バリデーション設計で大事な考え方(どこまでチェックするか)
「入口で落とせるものは入口で落とす」
バリデーションの基本方針は、
外からの入力は、入口でできるだけ厳しくチェックして、
中に「変なもの」を入れない
です。
パスパラメータ/クエリ/ボディの型やフォーマット
値の範囲(年齢・金額・件数など)
存在チェック(必須項目、空文字を許すかどうか)
日時の順序や、業務的な矛盾(終了日が開始日より前、など)
こういったチェックを、可能な限り Pydantic モデルに寄せておくと、
エンドポイントやサービス層のロジックがかなりスリムになります。
「どこまでをバリデーションとみなすか」
もうひとつ大事なのは、
「外部仕様に関わるルール」と
「内部実装寄りのルール」を分けること
です。
外部仕様寄り(クライアントと約束したいこと)
→ モデルのバリデーション(型、必須、範囲、フォーマット、相関チェック)
内部実装寄り(DB の存在確認、API 呼び出し先の状態など)
→ エンドポイントの中やサービス層の処理
例えば、
「この user_id のユーザーが DB に存在するか」
→ これは DB アクセスが必要なので、バリデーションというよりビジネスロジック側の役割。
「user_id は正の整数であるべき」
→ これは Pydantic モデル側でチェックできる。
という分け方を意識しておくと、
どのチェックをどこに書くべきか迷いにくくなります。
まとめ(バリデーションは「Web アプリの第一防衛ライン」)
Python Web フレームワーク(特に FastAPI)におけるバリデーションを整理すると、こうなります。
- バリデーションは、「外部から来た値が想定どおりか」を入口でチェックし、変な値を中に入れないための門番。
- FastAPI では、パスパラメータやクエリの型ヒント(int, str, bool など)だけで、基本的な型バリデーションと必須/任意の扱いが手に入る。
- リクエストボディは Pydantic モデル(BaseModel)で形・型・必須/任意・範囲・フォーマットを宣言し、Pydantic にバリデーションを任せるのが本丸。
- Field や validator(field_validator)を使えば、「0 ≤ age ≤ 120」「終了日は開始日より後」などの業務ルールもモデル側に寄せられる。
- バリデーションの設計では、「入口で落とせるものは入口で落とす」「外部仕様のルールはモデル、内部の存在チェックなどはロジック」と役割分担する意識が重要。
