Аннотированные валидаторы¶
??? API "Документация по API" pydantic.functional_validators.WrapValidator
pydantic.functional_validators.PlainValidator
pydantic.functional_validators.BeforeValidator
pydantic.functional_validators.AfterValidator
Pydantic предоставляет возможность применять валидаторы с помощью Annotated
. Вы должны использовать это всякий раз, когда хотите привязать проверку к типу, а не к модели или полю.
from typing import Any, List
from typing_extensions import Annotated
from pydantic import BaseModel, ValidationError
from pydantic.functional_validators import AfterValidator
def check_squares(v: int) -> int:
assert v**0.5 % 1 == 0, f'{v} is not a square number'
return v
def double(v: Any) -> Any:
return v * 2
MyNumber = Annotated[int, AfterValidator(double), AfterValidator(check_squares)]
class DemoModel(BaseModel):
number: List[MyNumber]
print(DemoModel(number=[2, 8]))
#> number=[4, 16]
try:
DemoModel(number=[2, 4])
except ValidationError as e:
print(e)
"""
1 validation error for DemoModel
number.1
Assertion failed, 8 is not a square number
assert ((8 ** 0.5) % 1) == 0 [type=assertion_error, input_value=4, input_type=int]
"""
В этом примере мы использовали некоторые псевдонимы типов ( MyNumber = Annotated[...]
). Хотя это может помочь улучшить разборчивость кода, это не обязательно, вы можете использовать Annotated
непосредственно в подсказке типа поля модели. Эти псевдонимы типов также не являются фактическими типами, но вы можете использовать аналогичный подход с TypeAliasType
для создания реальных типов. См. «Пользовательские типы» для более подробного объяснения пользовательских типов.
Также стоит отметить, что вы можете вкладывать Annotated
внутрь других типов. В этом примере мы использовали это, чтобы применить проверку к внутренним элементам списка. Тот же подход можно использовать для ключей dict и т. д.
Валидаторы «До», «После», «Wrap» и «Plain»¶
Pydantic предоставляет несколько типов функций проверки:
After
валидаторы выполняют внутренний анализ Pydantic. Они, как правило, более типобезопасны и, следовательно, их легче реализовать.Before
валидаторы запускаются перед внутренним анализом и проверкой Pydantic (например, приведениеstr
кint
). Они более гибкие, чем валидаторыAfter
, поскольку они могут изменять необработанные входные данные, но им также приходится иметь дело с необработанными входными данными, которые теоретически могут быть любым произвольным объектом.Plain
валидаторы похожи на валидаторmode='before'
но они немедленно прекращают проверку, дальнейшие валидаторы не вызываются, и Pydantic не выполняет никакой внутренней проверки.- Валидаторы
Wrap
являются наиболее гибкими из всех. Вы можете запустить код до или после того, как Pydantic и другие валидаторы сделают свое дело, или вы можете немедленно прекратить проверку, как при успешном значении, так и при ошибке.
Вы можете использовать несколько валидаторов до, после или mode='wrap'
, но только один PlainValidator
, поскольку простой валидатор не будет вызывать какие-либо внутренние валидаторы.
Вот пример валидатора mode='wrap'
:
import json
from typing import Any, List
from typing_extensions import Annotated
from pydantic import (
BaseModel,
ValidationError,
ValidationInfo,
ValidatorFunctionWrapHandler,
)
from pydantic.functional_validators import WrapValidator
def maybe_strip_whitespace(
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> int:
if info.mode == 'json':
assert isinstance(v, str), 'In JSON mode the input must be a string!'
# you can call the handler multiple times
try:
return handler(v)
except ValidationError:
return handler(v.strip())
assert info.mode == 'python'
assert isinstance(v, int), 'In Python mode the input must be an int!'
# do no further validation
return v
MyNumber = Annotated[int, WrapValidator(maybe_strip_whitespace)]
class DemoModel(BaseModel):
number: List[MyNumber]
print(DemoModel(number=[2, 8]))
#> number=[2, 8]
print(DemoModel.model_validate_json(json.dumps({'number': [' 2 ', '8']})))
#> number=[2, 8]
try:
DemoModel(number=['2'])
except ValidationError as e:
print(e)
"""
1 validation error for DemoModel
number.0
Assertion failed, In Python mode the input must be an int!
assert False
+ where False = isinstance('2', int) [type=assertion_error, input_value='2', input_type=str]
"""
Те же «режимы» применяются к @field_validator
, который обсуждается в следующем разделе.
Порядок валидаторов в Annotated
¶
Порядок проверки метаданных в Annotated
вопросах. Проверка идет справа налево и обратно. То есть, он запускает справа налево все валидаторы «до» (или вызывает валидаторы «обертки»), а затем слева направо возвращается обратно, вызывая все валидаторы «после».
from typing import Any, Callable, List, cast
from typing_extensions import Annotated, TypedDict
from pydantic import (
AfterValidator,
BaseModel,
BeforeValidator,
PlainValidator,
ValidationInfo,
ValidatorFunctionWrapHandler,
WrapValidator,
)
from pydantic.functional_validators import field_validator
class Context(TypedDict):
logs: List[str]
def make_validator(label: str) -> Callable[[Any, ValidationInfo], Any]:
def validator(v: Any, info: ValidationInfo) -> Any:
context = cast(Context, info.context)
context['logs'].append(label)
return v
return validator
def make_wrap_validator(
label: str,
) -> Callable[[Any, ValidatorFunctionWrapHandler, ValidationInfo], Any]:
def validator(
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> Any:
context = cast(Context, info.context)
context['logs'].append(f'{label}: pre')
result = handler(v)
context['logs'].append(f'{label}: post')
return result
return validator
class A(BaseModel):
x: Annotated[
str,
BeforeValidator(make_validator('before-1')),
AfterValidator(make_validator('after-1')),
WrapValidator(make_wrap_validator('wrap-1')),
BeforeValidator(make_validator('before-2')),
AfterValidator(make_validator('after-2')),
WrapValidator(make_wrap_validator('wrap-2')),
BeforeValidator(make_validator('before-3')),
AfterValidator(make_validator('after-3')),
WrapValidator(make_wrap_validator('wrap-3')),
BeforeValidator(make_validator('before-4')),
AfterValidator(make_validator('after-4')),
WrapValidator(make_wrap_validator('wrap-4')),
]
y: Annotated[
str,
BeforeValidator(make_validator('before-1')),
AfterValidator(make_validator('after-1')),
WrapValidator(make_wrap_validator('wrap-1')),
BeforeValidator(make_validator('before-2')),
AfterValidator(make_validator('after-2')),
WrapValidator(make_wrap_validator('wrap-2')),
PlainValidator(make_validator('plain')),
BeforeValidator(make_validator('before-3')),
AfterValidator(make_validator('after-3')),
WrapValidator(make_wrap_validator('wrap-3')),
BeforeValidator(make_validator('before-4')),
AfterValidator(make_validator('after-4')),
WrapValidator(make_wrap_validator('wrap-4')),
]
val_x_before = field_validator('x', mode='before')(
make_validator('val_x before')
)
val_x_after = field_validator('x', mode='after')(
make_validator('val_x after')
)
val_y_wrap = field_validator('y', mode='wrap')(
make_wrap_validator('val_y wrap')
)
context = Context(logs=[])
A.model_validate({'x': 'abc', 'y': 'def'}, context=context)
print(context['logs'])
"""
[
'val_x before',
'wrap-4: pre',
'before-4',
'wrap-3: pre',
'before-3',
'wrap-2: pre',
'before-2',
'wrap-1: pre',
'before-1',
'after-1',
'wrap-1: post',
'after-2',
'wrap-2: post',
'after-3',
'wrap-3: post',
'after-4',
'wrap-4: post',
'val_x after',
'val_y wrap: pre',
'wrap-4: pre',
'before-4',
'wrap-3: pre',
'before-3',
'plain',
'after-3',
'wrap-3: post',
'after-4',
'wrap-4: post',
'val_y wrap: post',
]
"""
Проверка значений по умолчанию¶
Валидаторы не будут запускаться, если используется значение по умолчанию. Это относится как к валидаторам @field_validator
, так и к Annotated
валидаторам. Вы можете заставить их работать с помощью Field(validate_default=True)
. Установка validate_default
в True
имеет самое близкое поведение к использованию always=True
в validator
в Pydantic v1. Однако, как правило, лучше использовать @model_validator(mode='before')
где функция вызывается до вызова внутреннего валидатора.
from typing_extensions import Annotated
from pydantic import BaseModel, Field, field_validator
class Model(BaseModel):
x: str = 'abc'
y: Annotated[str, Field(validate_default=True)] = 'xyz'
@field_validator('x', 'y')
@classmethod
def double(cls, v: str) -> str:
return v * 2
print(Model())
#> x='abc' y='xyzxyz'
print(Model(x='foo'))
#> x='foofoo' y='xyzxyz'
print(Model(x='abc'))
#> x='abcabc' y='xyzxyz'
print(Model(x='foo', y='bar'))
#> x='foofoo' y='barbar'
Валидаторы полей¶
??? API "Документация по API" pydantic.functional_validators.field_validator
Если вы хотите прикрепить валидатор к определенному полю модели, вы можете использовать декоратор @field_validator
.
from pydantic import (
BaseModel,
ValidationError,
ValidationInfo,
field_validator,
)
class UserModel(BaseModel):
name: str
id: int
@field_validator('name')
@classmethod
def name_must_contain_space(cls, v: str) -> str:
if ' ' not in v:
raise ValueError('must contain a space')
return v.title()
# you can select multiple fields, or use '*' to select all fields
@field_validator('id', 'name')
@classmethod
def check_alphanumeric(cls, v: str, info: ValidationInfo) -> str:
if isinstance(v, str):
# info.field_name is the name of the field being validated
is_alphanumeric = v.replace(' ', '').isalnum()
assert is_alphanumeric, f'{info.field_name} must be alphanumeric'
return v
print(UserModel(name='John Doe', id=1))
#> name='John Doe' id=1
try:
UserModel(name='samuel', id=1)
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
name
Value error, must contain a space [type=value_error, input_value='samuel', input_type=str]
"""
try:
UserModel(name='John Doe', id='abc')
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
id
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
"""
try:
UserModel(name='John Doe!', id=1)
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
name
Assertion failed, name must be alphanumeric
assert False [type=assertion_error, input_value='John Doe!', input_type=str]
"""
Несколько замечаний по валидаторам:
@field_validator
являются «методами класса», поэтому первое значение аргумента, которое они получают, — это классUserModel
, а не экземплярUserModel
. Мы рекомендуем вам использовать для них декоратор@classmethod
под декоратором@field_validator
, чтобы обеспечить правильную проверку типов.- второй аргумент — это значение поля, которое нужно проверить; его можно назвать как угодно
- третий аргумент, если он присутствует, является экземпляром
pydantic.ValidationInfo
- валидаторы должны либо вернуть проанализированное значение, либо вызвать
ValueError
илиAssertionError
(можно использовать операторыassert
). - Один валидатор можно применить к нескольким полям, передав ему несколько имен полей.
- Один валидатор также можно вызвать для всех полей, передав специальное значение
'*'
.
!!! предупреждение. Если вы используете операторы assert
, имейте в виду, что запуск Python с флагом оптимизации -O
отключает assert
, и валидаторы перестанут работать .
!!! Примечание. FieldValidationInfo
устарел в версии 2.4, вместо этого используйте ValidationInfo
.
Если вы хотите получить доступ к значениям из другого поля внутри @field_validator
, это может быть возможно с помощью ValidationInfo.data
, который представляет собой преобразование имени поля в значение поля. Проверка выполняется при определении полей порядка, поэтому вы должны быть осторожны при использовании ValidationInfo.data
, чтобы не получить доступ к полю, которое еще не было проверено/заполнено — например, в приведенном выше коде вы не сможете получить доступ info.data['id']
из name_must_contain_space
. Однако в большинстве случаев, когда вы хотите выполнить проверку с использованием нескольких значений полей, лучше использовать @model_validator
, который обсуждается в разделе ниже.
Валидаторы моделей¶
??? API "Документация по API" pydantic.functional_validators.model_validator
Валидацию также можно выполнить для всех данных модели с помощью @model_validator
.
from typing import Any
from typing_extensions import Self
from pydantic import BaseModel, ValidationError, model_validator
class UserModel(BaseModel):
username: str
password1: str
password2: str
@model_validator(mode='before')
@classmethod
def check_card_number_omitted(cls, data: Any) -> Any:
if isinstance(data, dict):
assert (
'card_number' not in data
), 'card_number should not be included'
return data
@model_validator(mode='after')
def check_passwords_match(self) -> Self:
pw1 = self.password1
pw2 = self.password2
if pw1 is not None and pw2 is not None and pw1 != pw2:
raise ValueError('passwords do not match')
return self
print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
Value error, passwords do not match [type=value_error, input_value={'username': 'scolvin', '... 'password2': 'zxcvbn2'}, input_type=dict]
"""
try:
UserModel(
username='scolvin',
password1='zxcvbn',
password2='zxcvbn',
card_number='1234',
)
except ValidationError as e:
print(e)
"""
1 validation error for UserModel
Assertion failed, card_number should not be included
assert 'card_number' not in {'card_number': '1234', 'password1': 'zxcvbn', 'password2': 'zxcvbn', 'username': 'scolvin'} [type=assertion_error, input_value={'username': 'scolvin', '..., 'card_number': '1234'}, input_type=dict]
"""
!!! note «При проверке типа возвращаемого значения» Методы, украшенные @model_validator
должны возвращать экземпляр self в конце метода. В целях проверки типов вы можете использовать Self
из typing
или из резервного порта typing_extensions
в качестве возвращаемого типа декорированного метода. В контексте приведенного выше примера вы также можете использовать def check_passwords_match(self: 'UserModel') -> 'UserModel'
чтобы указать, что метод возвращает экземпляр модели.
!!! note «При наследовании» @model_validator
, определенный в базовом классе, будет вызываться во время проверки экземпляра подкласса.
Overriding a `@model_validator` in a subclass will override the base class' `@model_validator`, and thus only the subclass' version of said `@model_validator` will be called.
Валидаторами модели могут быть mode='before'
, mode='after'
или mode='wrap'
.
Перед передачей валидаторам модели необработанные входные данные, которые часто представляют собой dict[str, Any]
но также могут быть экземпляром самой модели (например, если UserModel.model_validate(UserModel.construct(...))
называется) или что-нибудь еще, поскольку вы можете передавать произвольные объекты в model_validate
. Из-за этого mode='before'
валидаторы чрезвычайно гибки и мощны, но могут быть громоздкими и подверженными ошибкам при реализации. Перед валидаторами модели должны быть методы класса. Первый аргумент должен быть cls
(и мы также рекомендуем вам использовать @classmethod
ниже @model_validator
для правильной проверки типа), второй аргумент будет входным (обычно вам следует ввести его как Any
и использовать isinstance
для сужения типа) и третий аргумент. Аргументом (если он присутствует) будет pydantic.ValidationInfo
.
Валидаторы mode='after'
являются методами экземпляра и всегда получают экземпляр модели в качестве первого аргумента. Обязательно верните экземпляр в конце вашего валидатора. Не следует использовать (cls, ModelType)
в качестве подписи, вместо этого просто используйте (self)
и позвольте средствам проверки типов определить тип self
за вас. Поскольку они полностью типобезопасны, их зачастую проще реализовать, чем валидаторы mode='before'
. Если какое-либо поле не удалось проверить, валидаторы mode='after'
для этого поля вызываться не будут.
Обработка ошибок в валидаторах¶
Как упоминалось в предыдущих разделах, вы можете вызвать ValueError
или AssertionError
(включая те, которые сгенерированы операторами assert ...
) в валидаторе, чтобы указать, что проверка не удалась. Вы также можете вызвать PydanticCustomError
, который немного более многословен, но дает вам дополнительную гибкость. Любые другие ошибки (включая TypeError
) всплывают и не заключаются в ValidationError
.
from pydantic_core import PydanticCustomError
from pydantic import BaseModel, ValidationError, field_validator
class Model(BaseModel):
x: int
@field_validator('x')
@classmethod
def validate_x(cls, v: int) -> int:
if v % 42 == 0:
raise PydanticCustomError(
'the_answer_error',
'{number} is the answer!',
{'number': v},
)
return v
try:
Model(x=42 * 2)
except ValidationError as e:
print(e)
"""
1 validation error for Model
x
84 is the answer! [type=the_answer_error, input_value=84, input_type=int]
"""
Специальные типы¶
Pydantic предоставляет несколько специальных типов, которые можно использовать для настройки проверки.
-
InstanceOf
— это тип, который можно использовать для проверки того, что значение является экземпляром данного класса.from typing import List
from pydantic import BaseModel, InstanceOf, ValidationError
class Fruit: def repr(self): return self.class.name
class Banana(Fruit): ...
class Apple(Fruit): ...
class Basket(BaseModel): fruits: List[InstanceOf[Fruit]]
print(Basket(fruits=[Banana(), Apple()]))
> fruits=[Banana, Apple]¶
try: Basket(fruits=[Banana(), 'Apple']) except ValidationError as e: print(e) """ 1 validation error for Basket fruits.1 Input should be an instance of Fruit [type=is_instance_of, input_value='Apple', input_type=str] """
-
SkipValidation
— это тип, который можно использовать для пропуска проверки поля.from typing import List
from pydantic import BaseModel, SkipValidation
class Model(BaseModel): names: List[SkipValidation[str]]
m = Model(names=['foo', 'bar']) print(m)
> names=['foo', 'bar']¶
m = Model(names=['foo', 123]) # (1)! print(m)
> names=['foo', 123]¶
-
Обратите внимание, что проверка второго элемента пропускается. Если он имеет неправильный тип, во время сериализации будет выдано предупреждение.
Полевые проверки¶
Во время создания класса валидаторы проверяются, чтобы подтвердить, что указанные ими поля действительно существуют в модели.
Это может быть нежелательно, если, например, вы хотите определить валидатор для проверки полей, которые будут присутствовать только в подклассах модели, в которой определен валидатор.
Если вы хотите отключить эти проверки во время создания класса, вы можете передать валидатору check_fields=False
в качестве аргумента ключевого слова.
Валидаторы классов данных¶
Валидаторы также работают с классами данных Pydantic.
from pydantic import field_validator
from pydantic.dataclasses import dataclass
@dataclass
class DemoDataclass:
product_id: str # should be a five-digit string, may have leading zeros
@field_validator('product_id', mode='before')
@classmethod
def convert_int_serial(cls, v):
if isinstance(v, int):
v = str(v).zfill(5)
return v
print(DemoDataclass(product_id='01234'))
#> DemoDataclass(product_id='01234')
print(DemoDataclass(product_id=2468))
#> DemoDataclass(product_id='02468')
Контекст проверки¶
Вы можете передать объект контекста методам проверки, доступ к которому можно получить из аргумента info
для декорированных функций валидатора:
from pydantic import BaseModel, ValidationInfo, field_validator
class Model(BaseModel):
text: str
@field_validator('text')
@classmethod
def remove_stopwords(cls, v: str, info: ValidationInfo):
context = info.context
if context:
stopwords = context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
data = {'text': 'This is an example document'}
print(Model.model_validate(data)) # no context
#> text='This is an example document'
print(Model.model_validate(data, context={'stopwords': ['this', 'is', 'an']}))
#> text='example document'
print(Model.model_validate(data, context={'stopwords': ['document']}))
#> text='This is an example'
Это полезно, когда вам нужно динамически обновлять поведение проверки во время выполнения. Например, если вы хотите, чтобы поле имело динамически управляемый набор разрешенных значений, это можно сделать путем передачи разрешенных значений по контексту и наличия отдельного механизма для обновления разрешенных значений:
from typing import Any, Dict, List
from pydantic import (
BaseModel,
ValidationError,
ValidationInfo,
field_validator,
)
_allowed_choices = ['a', 'b', 'c']
def set_allowed_choices(allowed_choices: List[str]) -> None:
global _allowed_choices
_allowed_choices = allowed_choices
def get_context() -> Dict[str, Any]:
return {'allowed_choices': _allowed_choices}
class Model(BaseModel):
choice: str
@field_validator('choice')
@classmethod
def validate_choice(cls, v: str, info: ValidationInfo):
allowed_choices = info.context.get('allowed_choices')
if allowed_choices and v not in allowed_choices:
raise ValueError(f'choice must be one of {allowed_choices}')
return v
print(Model.model_validate({'choice': 'a'}, context=get_context()))
#> choice='a'
try:
print(Model.model_validate({'choice': 'd'}, context=get_context()))
except ValidationError as exc:
print(exc)
"""
1 validation error for Model
choice
Value error, choice must be one of ['a', 'b', 'c'] [type=value_error, input_value='d', input_type=str]
"""
set_allowed_choices(['b', 'c'])
try:
print(Model.model_validate({'choice': 'a'}, context=get_context()))
except ValidationError as exc:
print(exc)
"""
1 validation error for Model
choice
Value error, choice must be one of ['b', 'c'] [type=value_error, input_value='a', input_type=str]
"""
Аналогичным образом вы можете использовать контекст для сериализации .
Использование контекста проверки с инициализацией BaseModel
¶
Хотя нет возможности указать контекст в стандартном инициализаторе BaseModel
, эту проблему можно обойти, используя contextvars.ContextVar
и собственный метод __init__
:
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Dict, Iterator
from pydantic import BaseModel, ValidationInfo, field_validator
_init_context_var = ContextVar('_init_context_var', default=None)
@contextmanager
def init_context(value: Dict[str, Any]) -> Iterator[None]:
token = _init_context_var.set(value)
try:
yield
finally:
_init_context_var.reset(token)
class Model(BaseModel):
my_number: int
def __init__(self, /, **data: Any) -> None:
self.__pydantic_validator__.validate_python(
data,
self_instance=self,
context=_init_context_var.get(),
)
@field_validator('my_number')
@classmethod
def multiply_with_context(cls, value: int, info: ValidationInfo) -> int:
if info.context:
multiplier = info.context.get('multiplier', 1)
value = value * multiplier
return value
print(Model(my_number=2))
#> my_number=2
with init_context({'multiplier': 3}):
print(Model(my_number=2))
#> my_number=6
print(Model(my_number=2))
#> my_number=2
Повторное использование валидаторов¶
Иногда вам может понадобиться использовать один и тот же валидатор для нескольких полей/моделей (например, для нормализации некоторых входных данных). «Наивным» подходом было бы написать отдельную функцию, а затем вызывать ее из нескольких декораторов. Очевидно, что это влечет за собой большое количество повторений и шаблонного кода. Следующий подход демонстрирует, как можно повторно использовать валидатор, чтобы минимизировать избыточность и модели снова стали почти декларативными.
from pydantic import BaseModel, field_validator
def normalize(name: str) -> str:
return ' '.join((word.capitalize()) for word in name.split(' '))
class Producer(BaseModel):
name: str
_normalize_name = field_validator('name')(normalize)
class Consumer(BaseModel):
name: str
_normalize_name = field_validator('name')(normalize)
jane_doe = Producer(name='JaNe DOE')
print(repr(jane_doe))
#> Producer(name='Jane Doe')
john_doe = Consumer(name='joHN dOe')
print(repr(john_doe))
#> Consumer(name='John Doe')
本文总阅读量次