跳到内容

验证器

除了 Pydantic 的内置验证功能之外,您还可以利用字段和模型级别的自定义验证器来强制执行更复杂的约束,并确保数据的完整性。

提示

想要快速跳转到相关的验证器部分吗?

字段验证器

API 文档

pydantic.functional_validators.WrapValidator
pydantic.functional_validators.PlainValidator
pydantic.functional_validators.BeforeValidator
pydantic.functional_validators.AfterValidator
pydantic.functional_validators.field_validator

最简单的形式是,字段验证器是一个可调用对象,它接受要验证的值作为参数,并返回验证后的值。该可调用对象可以检查特定条件(参见引发验证错误)并更改验证后的值(强制转换或修改)。

可以使用四种不同类型的验证器。它们都可以使用注解模式定义,或者使用应用于field_validator()装饰器定义,应用于类方法

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

    以下是一个执行验证检查并返回未更改值的验证器示例。

    from typing import Annotated
    
    from pydantic import AfterValidator, BaseModel, ValidationError
    
    
    def is_even(value: int) -> int:
        if value % 2 == 1:
            raise ValueError(f'{value} is not an even number')
        return value  # (1)!
    
    
    class Model(BaseModel):
        number: Annotated[int, AfterValidator(is_even)]
    
    
    try:
        Model(number=1)
    except ValidationError as err:
        print(err)
        """
        1 validation error for Model
        number
          Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int]
        """
    
    1. 请注意,返回验证后的值非常重要。

    以下是另一个示例,展示了使用field_validator()装饰器执行验证检查并返回未更改的值。

    from pydantic import BaseModel, ValidationError, field_validator
    
    
    class Model(BaseModel):
        number: int
    
        @field_validator('number', mode='after')  # (1)!
        @classmethod
        def is_even(cls, value: int) -> int:
            if value % 2 == 1:
                raise ValueError(f'{value} is not an even number')
            return value  # (2)!
    
    
    try:
        Model(number=1)
    except ValidationError as err:
        print(err)
        """
        1 validation error for Model
        number
          Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int]
        """
    
    1. 'after'是装饰器的默认模式,可以省略。
    2. 请注意,返回验证后的值非常重要。
    修改值的示例

    以下示例展示了一个验证器如何更改验证后的值(未引发异常)。

    from typing import Annotated
    
    from pydantic import AfterValidator, BaseModel
    
    
    def double_number(value: int) -> int:
        return value * 2
    
    
    class Model(BaseModel):
        number: Annotated[int, AfterValidator(double_number)]
    
    
    print(Model(number=2))
    #> number=4
    
    from pydantic import BaseModel, field_validator
    
    
    class Model(BaseModel):
        number: int
    
        @field_validator('number', mode='after')  # (1)!
        @classmethod
        def double_number(cls, value: int) -> int:
            return value * 2
    
    
    print(Model(number=2))
    #> number=4
    
    1. 'after'是装饰器的默认模式,可以省略。

  • 前置验证器:在 Pydantic 的内部解析和验证之前运行(例如,将 str 强制转换为 int)。这些验证器比后置验证器更灵活,但它们也必须处理原始输入,理论上原始输入可以是任何任意对象。如果稍后在验证器函数中引发验证错误,则还应避免直接修改值,因为如果使用联合,修改后的值可能会传递给其他验证器。

    从该可调用对象返回的值随后将由 Pydantic 根据提供的类型注解进行验证。

    from typing import Annotated, Any
    
    from pydantic import BaseModel, BeforeValidator, ValidationError
    
    
    def ensure_list(value: Any) -> Any:  # (1)!
        if not isinstance(value, list):  # (2)!
            return [value]
        else:
            return value
    
    
    class Model(BaseModel):
        numbers: Annotated[list[int], BeforeValidator(ensure_list)]
    
    
    print(Model(numbers=2))
    #> numbers=[2]
    try:
        Model(numbers='str')
    except ValidationError as err:
        print(err)  # (3)!
        """
        1 validation error for Model
        numbers.0
          Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='str', input_type=str]
        """
    
    1. 请注意 value 的类型提示使用了Any前置验证器接受原始输入,可以是任何类型。

    2. 请注意,您可能需要检查其他通常可以成功验证 list 类型的序列类型(例如元组)。前置验证器为您提供了更大的灵活性,但您必须考虑每种可能的情况。

    3. 无论我们的 ensure_list 验证器是否对原始输入类型执行了操作,Pydantic 仍然会针对 int 类型执行验证。

    from typing import Any
    
    from pydantic import BaseModel, ValidationError, field_validator
    
    
    class Model(BaseModel):
        numbers: list[int]
    
        @field_validator('numbers', mode='before')
        @classmethod
        def ensure_list(cls, value: Any) -> Any:  # (1)!
            if not isinstance(value, list):  # (2)!
                return [value]
            else:
                return value
    
    
    print(Model(numbers=2))
    #> numbers=[2]
    try:
        Model(numbers='str')
    except ValidationError as err:
        print(err)  # (3)!
        """
        1 validation error for Model
        numbers.0
          Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='str', input_type=str]
        """
    
    1. 请注意 value 的类型提示使用了Any前置验证器接受原始输入,可以是任何类型。

    2. 请注意,您可能需要检查其他通常可以成功验证 list 类型的序列类型(例如元组)。前置验证器为您提供了更大的灵活性,但您必须考虑每种可能的情况。

    3. 无论我们的 ensure_list 验证器是否对原始输入类型执行了操作,Pydantic 仍然会针对 int 类型执行验证。

  • 普通验证器:行为类似于前置验证器,但它们会在返回后立即终止验证,因此不会调用进一步的验证器,并且 Pydantic 不会对字段类型执行任何内部验证。

    from typing import Annotated, Any
    
    from pydantic import BaseModel, PlainValidator
    
    
    def val_number(value: Any) -> Any:
        if isinstance(value, int):
            return value * 2
        else:
            return value
    
    
    class Model(BaseModel):
        number: Annotated[int, PlainValidator(val_number)]
    
    
    print(Model(number=4))
    #> number=8
    print(Model(number='invalid'))  # (1)!
    #> number='invalid'
    
    1. 尽管 'invalid' 不应通过 int 类型的验证,但 Pydantic 接受了该输入。
    from typing import Any
    
    from pydantic import BaseModel, field_validator
    
    
    class Model(BaseModel):
        number: int
    
        @field_validator('number', mode='plain')
        @classmethod
        def val_number(cls, value: Any) -> Any:
            if isinstance(value, int):
                return value * 2
            else:
                return value
    
    
    print(Model(number=4))
    #> number=8
    print(Model(number='invalid'))  # (1)!
    #> number='invalid'
    
    1. 尽管 'invalid' 不应通过 int 类型的验证,但 Pydantic 接受了该输入。

  • 包装验证器:是所有验证器中最灵活的。您可以在 Pydantic 和其他验证器处理输入之前或之后运行代码,或者您可以通过提前返回值或引发错误来立即终止验证。

    此类验证器必须使用一个强制性的额外 handler 参数来定义:一个可调用对象,它接受要验证的值作为参数。在内部,此处理程序会将值的验证委托给 Pydantic。您可以自由地将对处理程序的调用包装在 try..except 块中,或者根本不调用它。

    from typing import Any
    
    from typing import Annotated
    
    from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, WrapValidator
    
    
    def truncate(value: Any, handler: ValidatorFunctionWrapHandler) -> str:
        try:
            return handler(value)
        except ValidationError as err:
            if err.errors()[0]['type'] == 'string_too_long':
                return handler(value[:5])
            else:
                raise
    
    
    class Model(BaseModel):
        my_string: Annotated[str, Field(max_length=5), WrapValidator(truncate)]
    
    
    print(Model(my_string='abcde'))
    #> my_string='abcde'
    print(Model(my_string='abcdef'))
    #> my_string='abcde'
    
    from typing import Any
    
    from typing import Annotated
    
    from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, field_validator
    
    
    class Model(BaseModel):
        my_string: Annotated[str, Field(max_length=5)]
    
        @field_validator('my_string', mode='wrap')
        @classmethod
        def truncate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> str:
            try:
                return handler(value)
            except ValidationError as err:
                if err.errors()[0]['type'] == 'string_too_long':
                    return handler(value[:5])
                else:
                    raise
    
    
    print(Model(my_string='abcde'))
    #> my_string='abcde'
    print(Model(my_string='abcdef'))
    #> my_string='abcde'
    

默认值的验证

字段文档中所述,除非配置为这样做,否则字段的默认值不会被验证,因此自定义验证器也不会被应用。

使用哪种验证器模式

虽然这两种方法都可以实现相同的效果,但每种模式都提供了不同的优势。

使用注解模式

使用注解模式的关键优势之一是使验证器可重用

from typing import Annotated

from pydantic import AfterValidator, BaseModel


def is_even(value: int) -> int:
    if value % 2 == 1:
        raise ValueError(f'{value} is not an even number')
    return value


EvenNumber = Annotated[int, AfterValidator(is_even)]


class Model1(BaseModel):
    my_number: EvenNumber


class Model2(BaseModel):
    other_number: Annotated[EvenNumber, AfterValidator(lambda v: v + 2)]


class Model3(BaseModel):
    list_of_even_numbers: list[EvenNumber]  # (1)!
  1. 正如注解模式文档中提到的,我们还可以为注解的特定部分使用验证器(在本例中,验证应用于列表项,但不应用于整个列表)。

通过查看字段注解,也更容易理解哪些验证器应用于类型。

使用装饰器模式

使用field_validator()装饰器的关键优势之一是将函数应用于多个字段

from pydantic import BaseModel, field_validator


class Model(BaseModel):
    f1: str
    f2: str

    @field_validator('f1', 'f2', mode='before')
    @classmethod
    def capitalize(cls, value: str) -> str:
        return value.capitalize()

以下是关于装饰器用法的几个补充说明

  • 如果您希望验证器应用于所有字段(包括子类中定义的字段),则可以将 '*' 作为字段名称参数传递。
  • 默认情况下,装饰器将确保在模型上定义了提供的字段名称。如果您想在类创建期间禁用此检查,可以通过将 False 传递给 check_fields 参数来完成。当字段验证器在基类上定义,并且期望在子类上设置该字段时,这非常有用。

模型验证器

API 文档

pydantic.functional_validators.model_validator

还可以使用model_validator()装饰器对整个模型的数据执行验证。

可以使用三种不同类型的模型验证器

  • 后置验证器:在整个模型验证完成后运行。因此,它们被定义为实例方法,并且可以被视为后初始化钩子。重要提示:应返回验证后的实例。
    from typing_extensions import Self
    
    from pydantic import BaseModel, model_validator
    
    
    class UserModel(BaseModel):
        username: str
        password: str
        password_repeat: str
    
        @model_validator(mode='after')
        def check_passwords_match(self) -> Self:
            if self.password != self.password_repeat:
                raise ValueError('Passwords do not match')
            return self
    

  • 前置验证器:在模型实例化之前运行。这些验证器比后置验证器更灵活,但它们也必须处理原始输入,理论上原始输入可以是任何任意对象。如果稍后在验证器函数中引发验证错误,则还应避免直接修改值,因为如果使用联合,修改后的值可能会传递给其他验证器。

    from typing import Any
    
    from pydantic import BaseModel, model_validator
    
    
    class UserModel(BaseModel):
        username: str
    
        @model_validator(mode='before')
        @classmethod
        def check_card_number_not_present(cls, data: Any) -> Any:  # (1)!
            if isinstance(data, dict):  # (2)!
                if 'card_number' in data:
                    raise ValueError("'card_number' should not be included")
            return data
    

    1. 请注意 data 的类型提示使用了Any前置验证器接受原始输入,可以是任何类型。
    2. 大多数情况下,输入数据将是一个字典(例如,当调用 UserModel(username='...') 时)。但是,情况并非总是如此。例如,如果设置了from_attributes配置值,您可能会收到 data 参数的任意类实例。

  • 包装验证器:是所有验证器中最灵活的。您可以在 Pydantic 和其他验证器处理输入数据之前或之后运行代码,或者您可以通过提前返回数据或引发错误来立即终止验证。
    import logging
    from typing import Any
    
    from typing_extensions import Self
    
    from pydantic import BaseModel, ModelWrapValidatorHandler, ValidationError, model_validator
    
    
    class UserModel(BaseModel):
        username: str
    
        @model_validator(mode='wrap')
        @classmethod
        def log_failed_validation(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
            try:
                return handler(data)
            except ValidationError:
                logging.error('Model %s failed to validate with data %s', cls, data)
                raise
    

关于继承

在基类中定义的模型验证器将在子类实例的验证期间被调用。

在子类中覆盖模型验证器将覆盖基类的验证器,因此只会调用子类的验证器版本。

引发验证错误

要引发验证错误,可以使用三种类型的异常

  • ValueError:这是验证器内部最常引发的异常。
  • AssertionError:使用assert 语句也有效,但请注意,当 Python 在使用 -O 优化标志运行时,这些语句将被跳过。
  • PydanticCustomError:稍微冗长一些,但提供了额外的灵活性
    from pydantic_core import PydanticCustomError
    
    from pydantic import BaseModel, ValidationError, field_validator
    
    
    class Model(BaseModel):
        x: int
    
        @field_validator('x', mode='after')
        @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]
        """
    

验证信息

字段和模型验证器的可调用对象(在所有模式下)都可以选择接受额外的ValidationInfo参数,提供有用的额外信息,例如

验证数据

对于字段验证器,可以使用data属性访问已验证的数据。以下示例可以用作后置模型验证器示例的替代方案

from pydantic import BaseModel, ValidationInfo, field_validator


class UserModel(BaseModel):
    password: str
    password_repeat: str
    username: str

    @field_validator('password_repeat', mode='after')
    @classmethod
    def check_passwords_match(cls, value: str, info: ValidationInfo) -> str:
        if value != info.data['password']:
            raise ValueError('Passwords do not match')
        return value

警告

由于验证是按照字段定义的顺序执行的,因此您必须确保没有访问尚未验证的字段。例如,在上面的代码中,username 验证后的值尚不可用,因为它是在 password_repeat 之后定义的。

对于模型验证器data属性为 None

验证上下文

您可以将上下文对象传递给验证方法,可以使用context属性在验证器函数内部访问该上下文对象

from pydantic import BaseModel, ValidationInfo, field_validator


class Model(BaseModel):
    text: str

    @field_validator('text', mode='after')
    @classmethod
    def remove_stopwords(cls, v: str, info: ValidationInfo) -> str:
        if isinstance(info.context, dict):
            stopwords = info.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'

类似地,您可以使用上下文进行序列化

在直接实例化模型时提供上下文

目前无法在直接实例化模型时(即调用 Model(...) 时)提供上下文。您可以通过使用ContextVar和自定义 __init__ 方法来解决此问题

from __future__ import annotations

from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Generator

from pydantic import BaseModel, ValidationInfo, field_validator

_init_context_var = ContextVar('_init_context_var', default=None)


@contextmanager
def init_context(value: dict[str, Any]) -> Generator[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 isinstance(info.context, dict):
            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 AfterValidator, BaseModel, BeforeValidator, WrapValidator


class Model(BaseModel):
    name: Annotated[
        str,
        AfterValidator(runs_3rd),
        AfterValidator(runs_4th),
        BeforeValidator(runs_2nd),
        WrapValidator(runs_1st),
    ]

在内部,使用装饰器定义的验证器会转换为其注解形式的对应物,并在字段的现有元数据之后最后添加。这意味着相同的排序逻辑适用。

特殊类型

Pydantic 提供了一些特殊实用程序,可用于自定义验证。

  • InstanceOf 可用于验证值是否为给定类的实例。

    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 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. 请注意,跳过了第二个项目的验证。如果它的类型错误,则会在序列化期间发出警告。
  • PydanticUseDefault 可用于通知 Pydantic 应使用默认值。

    from typing import Annotated, Any
    
    from pydantic_core import PydanticUseDefault
    
    from pydantic import BaseModel, BeforeValidator
    
    
    def default_if_none(value: Any) -> Any:
        if value is None:
            raise PydanticUseDefault()
        return value
    
    
    class Model(BaseModel):
        name: Annotated[str, BeforeValidator(default_if_none)] = 'default_name'
    
    
    print(Model(name=None))
    #> name='default_name'
    

JSON Schema 和字段验证器

当使用前置普通包装字段验证器时,接受的输入类型可能与字段注解不同。

考虑以下示例

from typing import Any

from pydantic import BaseModel, field_validator


class Model(BaseModel):
    value: str

    @field_validator('value', mode='before')
    @classmethod
    def cast_ints(cls, value: Any) -> Any:
        if isinstance(value, int):
            return str(value)
        else:
            return value


print(Model(value='a'))
#> value='a'
print(Model(value=1))
#> value='1'

虽然 value 的类型提示是 str,但 cast_ints 验证器也允许整数。要指定正确的输入类型,可以提供 json_schema_input_type 参数

from typing import Any, Union

from pydantic import BaseModel, field_validator


class Model(BaseModel):
    value: str

    @field_validator(
        'value', mode='before', json_schema_input_type=Union[int, str]
    )
    @classmethod
    def cast_ints(cls, value: Any) -> Any:
        if isinstance(value, int):
            return str(value)
        else:
            return value


print(Model.model_json_schema()['properties']['value'])
#> {'anyOf': [{'type': 'integer'}, {'type': 'string'}], 'title': 'Value'}

为了方便起见,如果未提供参数,Pydantic 将使用字段类型(除非您使用的是普通验证器,在这种情况下,json_schema_input_type 默认为Any,因为字段类型被完全丢弃)。