目次
1. はじめに
FastAPIで作成したエンドポイントにてデータのバリデーションをpydanticで行っています。
ここで以下の2つを実現できるように改修したいと思います。
- データのバリデーションをデータの型だけでなく、指定した値と合致しているのかまで確認する
(ageという変数が整数なのかだけでなく、0<age<100となっているかというイメージ) - 指定した値と合致しない場合のエラーメッセージをカスタムする
2. 動作環境
fastapi 0.111.0
pydantic 2.7.1
3. フォルダ構成
project/
└─backend/
├─app/ # FastAPIによるエンドポイントの作成
├─routers
└─student.py
└─main.py
└─models # pydanticによるデータ認証
└─student_model.py
実行している内容としては以下の通りです。
-
main.py
FastAPIアプリケーションのインスタンス作成やルーターの統合を行います。 -
routers/student.py
エンドポイントを作成します。
studentの情報を登録します。
studentの登録する情報は「名前」「年齢」「専攻科目」になります。 -
models/student_model.py
routers/student.pyで登録するデータの認証を行います。
「名前」は文字列が入力されたか、「年齢」は値が整数かつ0以上100未満か、
「専攻科目」は値が文字列かつ選択肢に用意した値かを確認します。
4. FastAPIのエンドポイントの引数でバリデーションする場合
-
pydanticのモデルの作成
field_validatorデコレータを使用することで、バリデーションをカスタムすることができます。models/student_model.pyfrom pydantic import BaseModel, field_validator from enume import Enum # 専攻科目として受け付ける選択肢を定義する class Major(str, Enum): engineering = "工学" literature = "文学部" economics = "経済学" class RegisterStudent(BaseModel): name: str # ここでageのデータ型がint型か確認する age: int # ここで専攻科目のデータ型がstr型か確認する major: str # field_validatorを使用して、年齢が指定した範囲内かを確認する @field_validator("age") def validate_age(cls, age): if not (0 <= age < 100): # 指定範囲外の値が入力された場合のエラーメッセージ raise ValueError("年齢は0歳以上100歳未満で入力してください") return age # field_validatorを使用して、専攻科目は用意した選択肢に含まれているかを確認する @field_validator("major") def validate_major(cls, major): if major not in Major: # 選択肢以外値が入力された場合のエラーメッセージ raise ValueError("専攻科目は選択肢の値から選択してください") return major -
エンドポイントの改修
エンドポイントにて先ほど作成した「RegisterStudent」モデルを使用するようにします。app/routers/user.pyfrom fastapi import APIRouter, HTTPException, Depends # 作成したpydanticのモデルをインポート app.models.student_model import RegisterStudent router = APIRouter() @router.post("/students", status_code=201) # 受け取る引数の型ヒントにpydanticのモデルを指定する def create_student(student_info:RegisterStudent, db: Session = Depends(get_db)): try: # 処理内容は省略 except Exception: raise HTTPException(status_code=500, detail="サーバーでエラーが発生しました") -
main.pyの改修
エンドポイントの引数にて行うバリデーションでエラーとなった場合に例外を受け取るようにします。
公式ドキュメントにて以下の記載があります。リクエストに無効なデータが含まれている場合、
FastAPI は内部的にRequestValidationErrorを発生させます。
また、そのためのデフォルトの例外ハンドラも含まれています。
これをオーバーライドするにはRequestValidationErrorをインポートして@app.exception_handler(RequestValidationError)と一緒に使用して例外ハンドラをデコレートします。
この例外ハンドラはRequestと例外を受け取ります。そのため、@app.exception_handler(RequestValidationError)を使用します。
app/main.pyfrom fastapi import FastAPI, APIRouter from app.routers.user import router as user_router # 以下の2つを例外をキャッチするために追加 from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse app = FastAPI() router = APIRouter() app.include_router(user_router) # 以下で例外をキャッチ @app.exception_handler(RequestValidationError) def validation_exception_handler(request, exc): if exc.errors(): match exc.errors()[0]["type"]: # int型でない場合のエラーをキャッチ case "int_parsing": return JSONResponse(status_code=422, content={"detail": "数値を入力してください"}) # str型でない場合のエラーをキャッチ case "string_type": return JSONResponse(status_code=422, content={"detail": "文字列を入力してください"}) # field_validatorでカスタムしたバリデーションで発生したエラーをキャッチ case "value_error": return JSONResponse(status_code=422, content={"detail": str(exc.errors()[0]["ctx"]["error"])}) case _: return JSONResponse(status_code=422, content={"detail": "入力データが正しくありません。入力データを確認してください"}) else: return JSONResponse(status_code=422, content={"detail": "入力データが正しくありません。入力データを確認してください"})
5.try~except文でバリデーションする場合
FastAPIのエンドポイントの引数ではなく、通常の処理で使用する場合は以下のように使用できます。
FastAPIにおいてデータのバリデーションは基本的にエンドポイントの引数で行うようですが、FastAPIを使わない場合や引数でのバリデーションができない場合では以下のように実現できます。
from fastapi import APIRouter, HTTPException, Depends
# pydanticのモデルをインポート(4.で作成したものと同じモデルを使用します)
app.models.user_model import RegisterUserInfo
router = APIRouter()
@router.post("/students", status_code=201)
# 引数の受け取り方を変更
def create_user(name:str, age:int, major:int db: Session = Depends(get_db)):
try:
# モデルにてデータをバリデーション
RegisterStudent(name, age, major)
# 以下の処理は省略
# バリデーションエラーを受け取る
except ValidationError as validate_e:
raise HTTPException(status_code=422, detail=str(validate_e.errors()[0]["ctx"]["error"]))
except Exception:
raise HTTPException(status_code=500,
detail="サーバーでエラーが発生しました")
6. 参考
https://fastapi.tiangolo.com/ja/tutorial/handling-errors/#fastapihttpexceptionstarlettehttpexception
https://docs.pydantic.dev/latest/errors/errors/#custom-errors