2023年9月28日木曜日

pydanticV2 の model_validate は逐次評価ではなくすべてを評価してから ValidationError が raise される

pydanticV2 の model_validate は逐次評価ではなくすべてを評価してから ValidationError が raise される

概要

特に context (FiledInfoValidator) と組み合わせる場合には注意が必要かなと思います

結論としてはエラーとなりそうなところがあればちゃんと事前に None チェックをしましょうという話です

環境

  • Python 3.11.3
  • fastapi 0.100.1
  • pydantic 2.1.1

サンプルコード

from fastapi import FastAPI
from pydantic import BaseModel, Field, FieldValidationInfo, field_validator

app = FastAPI()


class User(BaseModel):
    name: str = Field()
    age: int = Field()

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str, info: FieldValidationInfo):
        if info.context is None:
            return v
        print("validate_name")
        if v not in ["hawksnowlog"]:
            raise ValueError()
        return v

    @field_validator("age")
    @classmethod
    def validate_age_with_context(cls, v: int, info: FieldValidationInfo):
        if info.context is None:
            return v
        if "age_limit" not in info.context or not isinstance(
            info.context["age_limit"], int
        ):
            raise ValueError("info.context.age_limit が正しく設定されていません")
        # ここがポイント
        # 前の validate_name で ValueError になると raise されない
        # 更に info.data["name"] には何も設定されずに来るのでここで KeyError が発生してしまう
        if info.data["name"] not in ["hawksnowlog"]:
            raise ValueError()
        # もしやるなら以下のように None チェックしてから info.data["name"] を扱うようにする
        if info.data.get("name") is None:
            raise ValueError()
        if info.data["name"] not in ["hawksnowlog"]:
            raise ValueError()
        print("validate_age_with_context")
        limit = int(info.context["age_limit"])
        if v > limit:
            raise ValueError(f"age が limit={limit} を超えています")
        return v


@app.post("/")
async def test_context(user: User):
    # ルーティング時の User 生成時にコールされるバリデーションはどこかで raise すればエラーになる
    # model_validate の場合は raise 時にエラーとならずすべてが評価されたあとで raise される
    user = User.model_validate(
        {"name": "hawksnowlog2", "age": 0}, context={"age_limit": 9}
    )
    return user

ちょっと解説

基本はコードのコメント部分に書いてあるとおりです

model_validate と context を使ってバリデーションした場合には途中で raise ValueError したものが後ろに回されてまとめて raise されます
なのでバリデーションのコードの中で KeyError など予期せぬエラーが発生した場合にはそこでバリデーションが止まってしまい本当に取得したい ValidationError が取得できなくなってしまいます

なので None 参照や Key 参照をする場合はちゃんと存在するかチェックした上でバリデーションを組む必要があります

0 件のコメント:

コメントを投稿