Перейти к содержанию

Аннотированные валидаторы

??? 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')

本文总阅读量