Pydanticでバリデーションをスキップして任意のタイミングでバリデーションする

FastAPI で API を開発していて「リクエストを受けたタイミングではバリデーションはせずに別のタイミングでバリデーションをかけたい」といった要件があり、苦戦したので纏めておく。

前提

  • Python 3.11.3
  • Pydantic 2.6.4

SkipValidation

SkipValidation でバリデーションをスキップすることができるが、任意のタイミングでバリデーションを実行しても Warning は出力されるもののバリデーションエラーとはならずスキップされる。シンプルにバリデーションをしたくないケースにはマッチする。フィールド単位で指定する必要があるため、フィールド数が多かったりネストされたモデルの場合はコード量が増え見通しが悪くなりそう。

from pydantic import BaseModel, SkipValidation


class Item(BaseModel):
    name: SkipValidation[str]
    price: SkipValidation[float]

m1 = Item(name=1, price="foo")
print(m1)  # -> name=1 price='foo'

m1.model_validate(m1.model_dump())  # ->
# /path/to/pydantic/functional_validators.py:702: UserWarning: Pydantic serializer warnings:
#   Expected `str` but got `int` - serialized value may not be as expected
#   function=lambda v, h: h(v), schema=original_schema
# /path/to/pydantic/functional_validators.py:702: UserWarning: Pydantic serializer warnings:
#   Expected `float` but got `str` - serialized value may not be as expected
#   function=lambda v, h: h(v), schema=original_schema

docs.pydantic.dev

docs.pydantic.dev

model_construct

model_construct メソッドを利用することでやりたいことが実現できた。

from pydantic import BaseModel


class Item(BaseModel):
    name: str
    price: float


m1 = Item.model_construct(name=1, price="foo")
print(m1)  # -> name=1 price='foo'

m1.model_validate(m1.model_dump())  # -> ValidationError

デメリットとしては以下が挙げられる。ドキュメントに記載されている通り、バリデーション済みのデータ、信頼できるデータにのみ利用した方が良い。

  • フィールドを指定する際にエディタ補完が効かない
  • フィールドで定義していないものを指定できる
    • 例: Item.model_construct(name=1, price="foo", bar="bar")
  • バリデーションされていないデータのため後続で DB に INSERT するといった場合に型の不整合が発生する可能性がある
  • meta: MetaModel といったネストされたモデルがある場合はコード量が増え見通しが悪くなる
    • 例: Item.model_construct(name=1, price="foo", meta=Meta.model_construct(author=1))

docs.pydantic.dev

docs.pydantic.dev

おわりに

Pydanticでバリデーションをスキップして任意のタイミングでバリデーションする方法を紹介した。 この方法はあまり推奨されることではないため、プロダクトの要件に左右されると思うが本当にバリデーションをスキップする必要があるのかを考えたほうが健全。