概要
以下にサンプルコードを掲載しますが pydanticV2 を使っていて自作クラスを型に指定しその自作クラス側で FieldValidationInfo を使ってバリデーションした場合に info.data が空になるという問題があります
解決する方法としてクラスをまとめるという方法がありますがそれだとクラスが膨大になってしまいます
今回はその場合の解決策を紹介します
環境
- Python 3.11.3
- fastapi 0.100.1
- pydantic 2.1.1
サンプルコード (info.data が空になる)
以下の場合 name -> age という順番でバリデーションされます
pydantic V2 の場合、バリデーションを通過した値はすべて FieldValidationInfo の info.data というオブジェクトに格納されます
しかし以下のように独自クラス側に age を分けている場合に info.data の値が空になるためバリデーションに成功した name を参照することができません
これを解決する方法として Age クラスを使わずに User 側にバリデーションを寄せる方法がありますがそれだとクラスが大きくなり微妙です
from pydantic import BaseModel, Field, FieldValidationInfo, field_validator
class Age(BaseModel):
age: int = Field()
@field_validator("age")
@classmethod
def validate_age(cls, v: int, info: FieldValidationInfo):
# print(info.data["name"]) ## KeyError になる
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 が正しく設定されていません")
print("validate_age_with_context")
limit = int(info.context["age_limit"])
if v > limit:
raise ValueError(f"age が limit={limit} を超えています")
return v
class User(BaseModel):
name: str = Field()
age: Age = Field()
@field_validator("name")
@classmethod
def validate_name(cls, v: str, info: FieldValidationInfo):
if info.context is None:
return v
if "allowed_name" not in info.context or not isinstance(
info.context["allowed_name"], str
):
raise ValueError("info.context.allowed_name が正しく設定されていません")
print("validate_name")
if v not in [info.context["allowed_name"]]:
raise ValueError()
return v
if __name__ == "__main__":
user = User.model_validate(
{"name": "hawksnowlog", "age": {"age": 0}}, context={"allowed_name": "hawksnowlog", "age_limit": 9}
)
解決方法 (context を無理やり使う
自分が発見した方法は info.context を使う方法です
これをすることで info.data ではなく info.context を Age 側で参照することで検証済みの name を使うことができます
from pydantic import BaseModel, Field, FieldValidationInfo, field_validator
class Age(BaseModel):
age: int = Field()
@field_validator("age")
@classmethod
def validate_age(cls, v: int, info: FieldValidationInfo):
# info.context を使うように変更
if info.context is None:
raise ValueError()
print(info.context["validated_name"])
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 が正しく設定されていません")
print("validate_age_with_context")
limit = int(info.context["age_limit"])
if v > limit:
raise ValueError(f"age が limit={limit} を超えています")
return v
class User(BaseModel):
name: str = Field()
age: Age = Field()
@field_validator("name")
@classmethod
def validate_name(cls, v: str, info: FieldValidationInfo):
if info.context is None:
return v
if "allowed_name" not in info.context or not isinstance(
info.context["allowed_name"], str
):
raise ValueError("info.context.allowed_name が正しく設定されていません")
print("validate_name")
if v not in [info.context["allowed_name"]]:
raise ValueError()
return v
# mode=before にすることで Field() に記載されている評価よりも前にこっちを実行させる
@field_validator("age", mode="before")
@classmethod
def validate_age(cls, v: Age, info: FieldValidationInfo):
if info.context is None:
raise ValueError()
# 同じクラス内では info.data が引き継がれるので検証済みの name が参照できる
info.context["validated_name"] = info.data["name"]
Age.model_validate(v, context=info.context)
return v
if __name__ == "__main__":
user = User.model_validate(
{"name": "hawksnowlog", "age": {"age": 0}},
context={"allowed_name": "hawksnowlog", "age_limit": 9},
)
最後に
他に自作クラス側に info.data を渡す方法はあるのだろうか
info.data の説明を見ると同一クラス内で参照可能とあるので Age クラスと User が継承するようにすれば参照できるようになるはず
なので継承しない独自クラスを作る場合には別のモデルクラスとフィールドが相互参照するようなモデルの設計をしないほうが無難な気がする