1
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?

More than 1 year has passed since last update.

pydanticにてエラーメッセージをカスタムする方法

1
Last updated at Posted at 2024-12-21

目次

  1. はじめに
  2. 動作環境
  3. フォルダ構成
  4. FastAPIのエンドポイントの引数でバリデーションする場合
  5. try~except文でバリデーションする場合
  6. 参考

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.py
    from 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.py
    from 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.py
    from 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を使わない場合や引数でのバリデーションができない場合では以下のように実現できます。

app/routers/user.py
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

1
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
1
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?