Pydanticでフィールド間の値を検証する

はじめに

こんにちは、物件連動チームのコヤマです。

私たちのシステムでは、物件データのETL処理にPydanticを活用しています。物件情報には多様な条件や規則が存在するため、単一項目の型定義だけでは表現しきれない場合があります。そのような場合に、@validatorデコレータを使用してカスタムバリデーションを実装することで、複雑な制約を表現できます。本記事では、フィールド間のバリデーションについて紹介します。

キーワード

@validator

@validatorは、Pydanticのデコレータで、データモデルのフィールドに対してカスタムバリデーションを適用するために使用されます。このデコレータを使用することで、データが特定の条件に従っているかどうかを検証できます。

フィールド間のカスタムバリデーション

valuesfields使って、他のフィールドの値に基づいた検証を行うことができます。

values

valuesは、検証済のフィールドの名前と値が含まれまるオブジェクトです。

バリデーションはフィールドの定義順で行われます。そのため、valuesを使用する際に、まだ検証されてないフィールドの名前や値を参照することはできません。(参考

例えば、物件のタイプによって

  • 賃貸物件の場合、賃料は0より大きく、1000000より小さい値でなければならない
  • 売買物件の場合、賃料は0でなければならない。

のような制約を設けたい場合は、以下のように書けます。

from pydantic import BaseModel, validator
from typing import Literal

class Property(BaseModel):
    type: Literal["chintai", "baibai"]  # 物件タイプ
    rent: int  # 賃料
    sale_price: int  # 価格 

    @validator("rent")
    def check_rent(cls, rent, values):
        property_type = values.get("type")  # typeの値を取得
        if property_type == "chintai":
            if rent < 0 or rent > 1000000:
                raise ValueError("賃貸物件の賃料は0より大きく、1000000より小さい値にしてください")
        elif property_type == "baibai":
            if rent != 0:
                raise ValueError("賃貸物件の場合、価格は0にしてください")
        return rent

実際に適切な値を入力するとvalidationが通り

valid_chintai_property = Property(type="chintai", rent=50000, sale_price=0)
print(valid_chintai_property)

# Property(type='chintai', rent=50000, sale_price=0)

不正値を入力すると、validation_errorとなります。

invalid_chintai_property = Property(type="chintai", rent=50000000000, sale_price=0)
print(invalid_chintai_property)

# ValidationError: 1 validation error for Property
# rent
#   賃貸物件の賃料は0より大きく、1000000より小さい値にしてください (type=value_error)のk

check_rentメソッドでのvaluesの中身を確認すると、以下のように現在処理されているフィールドの名前と値が含まれていました。

values = {'type': 'chintai'}

ただし、valuesには、sale_priceの情報が含まれていないことに注意してください。従って、この例ではvalues.get("sale_price”)のような、価格値を使用した検証はできません。

この挙動を避けるために、@root_validatorを使用する方法もあります。@root_validatorを使用すると、全てのフィールド名と値を参照できます。

field

fieldは、フィールドに関する情報(型、デフォルト値など)を持っています。

例えば、物件タイプが指定されてない時に

  • 賃料はNoneを返す
  • 価格は「相談」を返す

のようにしたい場合は、以下のように書けます。

from pydantic import BaseModel, validator
from typing import Literal, Optional, Union

class Property(BaseModel):
    type: Literal["none", "chintai", "baibai"]  # noneを追加
    rent: Optional[int]
    sale_price: Union[int, str]

    @validator("rent", "sale_price")
    def set_value_for_none_property_type(cls, value, values, field):
        property_type = values.get("type")
        if property_type == "none":
            if field.name == "rent": # 現在のフィールド名で分岐
                return None
            elif field.name == "sale_price":
                return "相談"
        return value

property_typeをnoneにしてみると、

Property(type="none", rent=3000, sale_price=2000000)

# Property(type='none', rent=None, sale_price='相談')

賃料はNoneで、価格は「相談」に変換されました。

set_value_for_none_property_typeメソッドでのfieldの値は以下のようになっていました。

rentの時 
ModelField(name='rent', type=Optional[int], required=False, default=None)

sale_priceの時
ModelField(name='sale_price', type=Union[int, str], required=True)

フィールド名や型情報の他に、フィールドが必須かどうかを示すrequiredや、デフォルト値の情報が含まれていました。

まとめ

@validatorを活用することで、フィールド間の複雑な検証も簡単に実現できました。

@validator以外にもPydanticには多くの機能が存在するので、学習してさらに理解を深めていこうと思います!