跳转至

验证器

Annotated Validators 标注验证人

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 嵌套在其他类型中。在这个例子中,我们使用它来对列表的内部项应用验证。同样的方法也可以用于字典键等。

之前、之后、包装和普通验证器

Pydantic 提供了多种类型的验证器函数:

  • After 验证器在 Pydantic 的内部解析之后运行。它们通常更具类型安全性,因此更容易实现。

  • Before 验证器在 Pydantic 的内部解析和验证(例如将 str 强制转换为 int )之前运行。这些验证器比 After 验证器更灵活,因为它们可以修改原始输入,但它们也必须处理原始输入,这在理论上可以是任何任意对象。

  • Plain 验证器类似于 mode='before' 验证器,但它们会立即终止验证,不会调用其他验证器,并且 Pydantic 不会执行任何内部验证。

  • Wrap 验证器是所有验证器中最灵活的。你可以在 Pydantic 和其他验证器执行操作之前或之后运行代码,或者你可以立即终止验证,无论是使用成功的值还是错误。

可以使用多个 before、after 或 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 与在 Pydantic v1 中使用 always=Truevalidator 中的行为最接近。但是,通常最好在调用内部验证器之前调用函数的 @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'

Field validators

字段验证程序

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

需要注意的是验证人:

  • @s 是“类方法”,因此它们接收的第一个参数值是 UserModel 类,而不是 UserModel 的实例。我们建议你在 @field_validator 装饰器下面使用 @classmethod 装饰器来获得正确的类型检查。

  • 第二个参数是要验证的字段值;它可以根据需要命名

  • 第三个参数,如果存在,是 pydantic.ValidationInfo 的一个实例

  • 验证器应返回解析后的值,或引发 ValueErrorAssertionError (可以使用 assert 语句)。

  • 一个单一的验证器可以通过传递多个字段名称应用于多个字段。

  • 也可以通过传递特殊值 '*' 来调用单个验证程序对所有字段进行验证。

警告

如果使用 assert 语句,请记住,使用 -O 优化标志运行 Python 会禁用 assert 语句,并且验证器将停止工作。

注意

FieldValidationInfo 在 2.4 中已弃用,改为使用 ValidationInfo

如果要从 @field_validator 中的另一个字段访问值,可以使用 ValidationInfo.data ,它是一个字段名到字段值的字典。验证是按字段定义的顺序进行的,因此在使用 ValidationInfo.data 时要小心,不要访问尚未验证/填充的字段——例如,在上面的代码中,您将无法从 name_must_contain_space 内部访问 info.data['id'] 。但是,在大多数情况下,您希望使用 @model_validator 进行多个字段值的验证,这将在下面的部分中讨论。

Model validators

模型验证器

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

注意

“关于返回类型检查” 用 @model_validator 修饰的方法应该在方法结束时返回自身实例。为了进行类型检查,可以使用 Self 来自 typingtyping_extensions 的回溯作为修饰方法的返回类型。在上述示例的上下文中,也可以使用 def check_passwords_match(self: 'UserModel') -> 'UserModel' 来表示该方法返回模型的实例。

注意

“关于继承” 在基类中定义的 @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 (我们还建议你在 @model_validator 中使用 @classmethod 进行适当的类型检查),第二个参数将是输入(你通常应该将其类型化为 Any ,并使用 isinstance 来缩小类型),第三个参数(如果存在)将是一个 pydantic.ValidationInfo

mode='after' 验证器是实例方法,并且始终将模型的实例作为第一个参数接收。请确保在验证器的末尾返回该实例。您不应该使用 (cls, ModelType) 作为签名,而是只需使用 (self) ,并让类型检查器为您推断 self 的类型。由于这些是完全类型安全的,因此它们通常比 mode='before' 验证器更容易实现。如果任何字段验证失败,将不会调用该字段的 mode='after' 验证器。

处理验证器中的错误

如前几节所述,您可以在验证器中引发 ValueErrorAssertionError (包括由 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]
    """

Special Types

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]
1.
请注意,第二项的验证被跳过。如果它的类型错误,在序列化期间将发出警告。

实地考察

在创建类时,会检查验证器以确认它们指定的字段实际上存在于模型上。

如果例如,您希望定义一个验证器来验证仅在定义验证器的模型的子类上才会存在的字段,那么这可能是不希望的。

如果您想在创建类时禁用这些检查,可以将 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

Reusing Validators

有时,您可能希望在多个字段/模型上使用相同的验证器(例如,对某些输入数据进行规范化)。“天真”的方法是编写一个单独的函数,然后从多个装饰器中调用它。显然,这需要大量的重复和样板代码。以下方法演示了如何重用验证器,从而最大限度地减少冗余并使模型再次几乎是声明性的。

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


本文总阅读量