跳转至

验证器

警告

“🚧 正在进行中” 此页面正在进行中。

此页面提供了在 Pydantic 中创建更复杂、自定义验证器的示例代码片段。

使用带有 Annotated 元数据的自定义验证器

在这个示例中,我们将构建一个自定义验证器,附加到一个 Annotated 类型上,以确保 datetime 对象遵守给定的时区约束。

自定义验证器支持时区的字符串指定,如果 datetime 对象没有正确的时区,它将引发错误。

我们在验证器中使用 __get_pydantic_core_schema__ 来定制已注释类型的模式(在这种情况下, datetime ),这使我们能够添加自定义验证逻辑。值得注意的是,我们使用 wrap 验证器函数,以便我们可以在默认的 pydantic datetime 进行验证之前和之后执行操作。

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'}]
    """
  1. handler 函数是我们用来验证输入的标准 pydantic 验证

  2. 我们在此包装验证器中调用 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'}]
    """

验证嵌套模型字段

在这里,我们展示了两种验证嵌套模型字段的方法,其中验证器利用了来自父模型的数据。

在这个例子中,我们构建了一个验证器,用于检查每个用户的密码是否不在父模型指定的禁止密码列表中。

有一种方法是在外部模型上放置一个自定义验证器:

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] """

请注意,如果上下文属性未包含在 model_validate 中,那么 info.context 将是 None ,并且禁止密码列表将不会被添加到上述实现中的上下文。因此, validate_user_passwords 将不会执行所需的密码验证。

有关验证上下文的更多详细信息可以在这里找到。


本文总阅读量