Валидаторы
!!! предупреждение «🚧 В работе» Эта страница находится в стадии разработки.
На этой странице представлены фрагменты примеров для создания более сложных пользовательских валидаторов в 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'}]
"""
- Функция
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'}]
"""
Проверка вложенных полей модели¶
Здесь мы демонстрируем два способа проверки поля вложенной модели, где валидатор использует данные из родительской модели.
В этом примере мы создаем валидатор, который проверяет, что пароль каждого пользователя не находится в списке запрещенных паролей, указанном родительской моделью.
Один из способов сделать это — разместить собственный валидатор во внешней модели:
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
не будет выполнять желаемую проверку пароля.
Более подробную информацию о контексте проверки можно найти здесь .
本文总阅读量次