ヒトリ歩き

愚痴とかいろいろ書きます

フィールドとモデルのバリデーション - pydanticの活用方法

pydanticのバリデーションで簡単にバリデーションが出来るようにデコレータが提供されている。

@field_validatorデコレータは、モデルの特定のフィールドに対して検証をしたい場合に使用する。
@model_validatorデコレータは、モデルのデータ全体に対して検証をしたい場合に使用する。

フィールドだけで完結するバリデーションはfield_validatorデコレータを使ったほうがいい。 モデルのフィールド同士の組み合わせでバリデーションをしたい場合はmodel_validatorデコレータを使用すると良い。

肉とソースの組み合わせでカルビに合わないソースが来たら、エラーにするモデルを作るとこんな感じ。

from pydantic import BaseModel, model_validator
from typing import Any

class Yakiniku(BaseModel):
    beaf: str
    source: str

    @model_validator(mode="after")
    def check(self):
        if self.beaf == "カルビ":
            if self.source == "タレ" or self.source == "レモン":
                return self
            else:
                raise ValueError("おかしな組み合わせ")
        raise ValueError("カルビ以外嫌い")


print(Yakiniku(beaf="カルビ", source="レモン"))
print(Yakiniku(beaf="カルビ", source="タレ"))
print(Yakiniku(beaf="カルビ", source="チョコ"))

カルビとレモンまたはカルビとタレの組み合わせであれば、インスタンスは生成可能。 それ以外の場合、ValidationErrorになる。

beaf='カルビ' source='レモン'
beaf='カルビ' source='タレ'
Traceback (most recent call last):
  File "/Users/kotaro/github/pythonStudy/src/pydantic/model_fileld.py", line 21, in <module>
    print(Yakiniku(beaf="カルビ", source="チョコ"))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/kotaro/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pydantic/main.py", line 164, in __init__
    __pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Yakiniku
  Value error, おかしな組み合わせ [type=value_error, input_value={'beaf': 'カルビ', 'source': 'チョコ'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.5/v/value_error

エイリアスを使用して、バリデーションを実現する方法もある。 Annotatedに、AfterValidatorまたはBaforeValidatorに検証で使用する関数を指定すればよい。

from typing import Optional, Any, Annotated
from pydantic.functional_validators import AfterValidator, BeforeValidator

def plus(v: Any) -> Any:
    print("Call plus function")
    return v + 1

def double(v: Any) -> Any:
    print("Call dobule function")
    return v * 2

MyNumber = Annotated[int, AfterValidator(double), BeforeValidator(plus)]

class DemoModel(BaseModel):
    number: MyNumber


print(DemoModel(2))

Before だと実際のデータに対するチェックができる。
上記のソースの場合、DemoModelのパラメータに"a"を指定するとplus関数には"a"が渡される。 return文で v + 1しているので、TypeErrorが発生する。

Afterだとpydanticでの型チェック後にバリデーションが実行される。 そのため、上記のソースの場合、DemoModelはint型を期待しているため、dobule関数を実行する前に 型違反でValidationErrorが発生することになる。

model_validatorデコレータは、modeでbefore/afterが指定できる。 その際に、beforeを指定するとクラスメソッドとして定義が必要なので、@classmothodを忘れずに

さいごに

フィールドやモデル単位でバリデーションが設定出来るので、データクラスを使用する側で違反していないかチェックする必要はない。そのため、データクラスを使用する側と使用される側で正しい責務に分けれる。 (そもそも、コンストラクタ内でチェックするから責務が分かれるのか・・・)
コンストラクタでチェックするにも処理が長くなるので、関数が分けれることもメリットだと感じた。 modeにはplainとwrapがあるけど、どんなものかはいつか調べる。

参考

docs.pydantic.dev