バリデーター
!!!警告「🚧 作業中」 このページは進行中です。
このページでは、Pydantic でより複雑なカスタム バリデータを作成するためのサンプル スニペットを提供します。
Annotated
メタデータでのカスタム バリデータの使用¶
この例では、Annotated
型にアタッチされたカスタム バリデータを構築し、datetime
オブジェクトが指定されたタイムゾーン制約に準拠していることを確認します。
カスタム バリデーターはタイムゾーンの文字列指定をサポートしており、datetime
オブジェクトに正しいタイムゾーンがない場合はエラーが発生します。
バリデーターで__get_pydantic_core_schema__
使用して、アノテーション付き型 (この場合は datetime
) のスキーマをカスタマイズします。これにより、カスタム検証ロジックを追加できるようになります。特に、datetime
のデフォルトのpydantic
検証の前後の両方で操作を実行できるように、 wrap
バリデーター関数を使用しています。
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable, Optional
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import (
GetCoreSchemaHandler,
PydanticUserError,
TypeAdapter,
ValidationError,
)
@dataclass(frozen=True)
class MyDatetimeValidator:
tz_constraint: Optional[str] = None
def tz_constraint_validator(
self,
value: dt.datetime,
handler: Callable, # (1)!
):
"""Validate tz_constraint and tz_info."""
# handle naive datetimes
if self.tz_constraint is None:
assert (
value.tzinfo is None
), 'tz_constraint is None, but provided value is tz-aware.'
return handler(value)
# validate tz_constraint and tz-aware tzinfo
if self.tz_constraint not in pytz.all_timezones:
raise PydanticUserError(
f'Invalid tz_constraint: {self.tz_constraint}',
code='unevaluable-type-annotation',
)
result = handler(value) # (2)!
assert self.tz_constraint == str(
result.tzinfo
), f'Invalid tzinfo: {str(result.tzinfo)}, expected: {self.tz_constraint}'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.tz_constraint_validator,
handler(source_type),
)
LA = 'America/Los_Angeles'
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(LA)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
except ValidationError as ve:
pprint(ve.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Invalid tzinfo: Europe/London, expected: America/Los_Angeles')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Invalid tzinfo: Europe/London, expected: America/Los_Angeles',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
handler
関数は、標準的なpydantic
検証で入力を検証するために呼び出すものです。- このラップバリデーターでは、
handler
関数を呼び出して、標準のpydantic
検証で入力を検証します。
同様の方法で UTC オフセット制約を強制することもできます。 lower_bound
とupper_bound
あると仮定すると、カスタム バリデーターを作成して、 datetime
が定義した境界内に含まれる UTC オフセットを持つことを確認できます。
import datetime as dt
from dataclasses import dataclass
from pprint import pprint
from typing import Any, Callable
import pytz
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated
from pydantic import GetCoreSchemaHandler, TypeAdapter, ValidationError
@dataclass(frozen=True)
class MyDatetimeValidator:
lower_bound: int
upper_bound: int
def validate_tz_bounds(self, value: dt.datetime, handler: Callable):
"""Validate and test bounds"""
assert value.utcoffset() is not None, 'UTC offset must exist'
assert self.lower_bound <= self.upper_bound, 'Invalid bounds'
result = handler(value)
hours_offset = value.utcoffset().total_seconds() / 3600
assert (
self.lower_bound <= hours_offset <= self.upper_bound
), 'Value out of bounds'
return result
def __get_pydantic_core_schema__(
self,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
return core_schema.no_info_wrap_validator_function(
self.validate_tz_bounds,
handler(source_type),
)
LA = 'America/Los_Angeles' # UTC-7 or UTC-8
ta = TypeAdapter(Annotated[dt.datetime, MyDatetimeValidator(-10, -5)])
print(
ta.validate_python(dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LA)))
)
#> 2023-01-01 00:00:00-07:53
LONDON = 'Europe/London'
try:
print(
ta.validate_python(
dt.datetime(2023, 1, 1, 0, 0, tzinfo=pytz.timezone(LONDON))
)
)
except ValidationError as e:
pprint(e.errors(), width=100)
"""
[{'ctx': {'error': AssertionError('Value out of bounds')},
'input': datetime.datetime(2023, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>),
'loc': (),
'msg': 'Assertion failed, Value out of bounds',
'type': 'assertion_error',
'url': 'https://errors.pydantic.dev/2.8/v/assertion_error'}]
"""
ネストされたモデルフィールドの検証¶
ここでは、バリデーターが親モデルのデータを利用する、ネストされたモデルのフィールドを検証する 2 つの方法を示します。
この例では、各ユーザーのパスワードが親モデルで指定された禁止パスワードのリストに含まれていないことを確認するバリデーターを構築します。
これを行う 1 つの方法は、外部モデルにカスタム バリデータを配置することです。
from typing import List
from typing_extensions import Self
from pydantic import BaseModel, ValidationError, model_validator
class User(BaseModel):
username: str
password: str
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@model_validator(mode='after')
def validate_user_passwords(self) -> Self:
"""Check that user password is not in forbidden list. Raise a validation error if a forbidden password is encountered."""
for user in self.users:
current_pw = user.password
if current_pw in self.forbidden_passwords:
raise ValueError(
f'Password {current_pw} is forbidden. Please choose another password for user {user.username}.'
)
return self
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization(**data)
except ValidationError as e:
print(e)
"""
1 validation error for Organization
Value error, Password 123 is forbidden. Please choose another password for user Spartacat. [type=value_error, input_value={'forbidden_passwords': [...gh', 'password': '87'}]}, input_type=dict]
"""
あるいは、ネストされたモデル クラス ( User
) でカスタム バリデーターを使用し、親モデルからの禁止されたパスワード データを検証コンテキスト経由で渡すこともできます。
!!!警告 バリデーター内でコンテキストを変更できる機能により、ネストされた検証に多くの機能が追加されますが、コードが混乱したり、デバッグが困難になったりする可能性もあります。このアプローチは自己責任で使用してください。
from typing import List
from pydantic import BaseModel, ValidationError, ValidationInfo, field_validator
class User(BaseModel):
username: str
password: str
@field_validator('password', mode='after')
@classmethod
def validate_user_passwords(
cls, password: str, info: ValidationInfo
) -> str:
"""Check that user password is not in forbidden list."""
forbidden_passwords = (
info.context.get('forbidden_passwords', []) if info.context else []
)
if password in forbidden_passwords:
raise ValueError(f'Password {password} is forbidden.')
return password
class Organization(BaseModel):
forbidden_passwords: List[str]
users: List[User]
@field_validator('forbidden_passwords', mode='after')
@classmethod
def add_context(cls, v: List[str], info: ValidationInfo) -> List[str]:
if info.context is not None:
info.context.update({'forbidden_passwords': v})
return v
data = {
'forbidden_passwords': ['123'],
'users': [
{'username': 'Spartacat', 'password': '123'},
{'username': 'Iceburgh', 'password': '87'},
],
}
try:
org = Organization.model_validate(data, context={})
except ValidationError as e:
print(e)
"""
1 validation error for Organization
users.0.password
Value error, Password 123 is forbidden. [type=value_error, input_value='123', input_type=str]
"""
context プロパティがmodel_validate
に含まれていない場合、 info.context
None
になり、上記の実装では禁止されたパスワードのリストがコンテキストに追加されないことに注意してください。そのため、 validate_user_passwords
必要なパスワード検証を実行しません。
検証コンテキストの詳細については、ここを参照してください。
本文总阅读量次