Python | Web フレームワーク:バリデーション

Python
スポンサーリンク

概要(バリデーション=「変な値は中に入れない門番」)

バリデーションは、

「外から送られてきた値が、想定どおりかチェックして、
おかしければ入口で止める」

仕組みです。

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)
Python

Field(..., 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}$")
Python

username
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」「終了日は開始日より後」などの業務ルールもモデル側に寄せられる。
  • バリデーションの設計では、「入口で落とせるものは入口で落とす」「外部仕様のルールはモデル、内部の存在チェックなどはロジック」と役割分担する意識が重要。

タイトルとURLをコピーしました