はじめに
こんにちは、物件連動チームのコヤマです。
私たちのシステムでは、物件データのETL処理にPydanticを活用しています。物件情報には多様な条件や規則が存在するため、単一項目の型定義だけでは表現しきれない場合があります。そのような場合に、@validator
デコレータを使用してカスタムバリデーションを実装することで、複雑な制約を表現できます。本記事では、フィールド間のバリデーションについて紹介します。
キーワード
@validator
@validator
は、Pydanticのデコレータで、データモデルのフィールドに対してカスタムバリデーションを適用するために使用されます。このデコレータを使用することで、データが特定の条件に従っているかどうかを検証できます。
フィールド間のカスタムバリデーション
values
やfields
使って、他のフィールドの値に基づいた検証を行うことができます。
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には多くの機能が存在するので、学習してさらに理解を深めていこうと思います!