验证器
除了 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] """
- 请注意,返回已验证的值很重要。
这是一个执行验证检查并返回未更改值的验证器示例,这次使用
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] """
'after'
是装饰器的默认模式,可以省略。- 请注意,返回已验证的值很重要。
示例:改变值
这是一个验证器更改已验证值(不引发异常)的示例。
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
'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] """
-
请注意将
Any
用作value
的类型提示。_前_验证器接受原始输入,它可以是任何类型。 -
请注意,您可能希望检查其他序列类型(例如元组),这些类型通常会成功验证为
list
类型。_前_验证器为您提供了更大的灵活性,但您必须考虑所有可能的情况。 -
Pydantic 仍然对
int
类型执行验证,无论我们的ensure_list
验证器是否对原始输入类型进行了操作。
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] """
-
请注意将
Any
用作value
的类型提示。_前_验证器接受原始输入,它可以是任何类型。 -
请注意,您可能希望检查其他序列类型(例如元组),这些类型通常会成功验证为
list
类型。_前_验证器为您提供了更大的灵活性,但您必须考虑所有可能的情况。 -
Pydantic 仍然对
int
类型执行验证,无论我们的ensure_list
验证器是否对原始输入类型进行了操作。
-
-
**_简单_验证器**:与_前_验证器类似,但它们在返回后会**立即终止验证**,因此不会调用其他验证器,并且 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'
- 尽管
'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'
- 尽管
'invalid'
不应针对int
类型进行验证,但 Pydantic 接受该输入。
- 尽管
-
**_包装_验证器**:是最灵活的。您可以在 Pydantic 和其他验证器处理输入之前或之后运行代码,或者您可以立即终止验证,无论是通过提前返回值还是通过引发错误。
此类验证器必须定义一个**强制性**的额外_处理程序_参数:一个可调用对象,它将要验证的值作为参数。在内部,此处理程序将验证值委托给 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)!
- 如注解模式文档中所述,我们还可以利用注解的特定部分的验证器(在这种情况下,验证应用于列表项,而不是整个列表)。
通过查看字段注解,也可以更容易地理解哪些验证器应用于某个类型。
使用装饰器模式¶
使用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
参数来完成。当字段验证器定义在基类上,并且预期字段存在于子类中时,这很有用。
模型验证器¶
还可以使用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
- 请注意将
Any
用作data
的类型提示。_前_验证器接受原始输入,它可以是任何类型。 - 大多数情况下,输入数据将是一个字典(例如,当调用
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
参数,提供有用的额外信息,例如
- 已验证数据
- 用户定义上下文
- 当前验证模式:
'python'
或'json'
(参见mode
属性) - 当前字段名,如果使用字段验证器(参见
field_name
属性)。
验证数据¶
对于字段验证器,可以使用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
_之后_定义的。
验证上下文¶
您可以将上下文对象传递给验证方法,可以使用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 collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any
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]
- 请注意,跳过了第二个项目的验证。如果它的类型不正确,则在序列化期间会发出警告。
-
ValidateAs
可用于从 Pydantic 原生支持的类型中验证自定义类型。当使用具有多个字段的自定义类型时,这特别有用。from typing import Annotated from pydantic import BaseModel, TypeAdapter, ValidateAs class MyCls: def __init__(self, a: int) -> None: self.a = a def __repr__(self) -> str: return f"MyCls(a={self.a})" class ValModel(BaseModel): a: int ta = TypeAdapter( Annotated[MyCls, ValidateAs(ValModel, lambda v: MyCls(a=v.a))] ) print(ta.validate_python({'a': 1})) #> MyCls(a=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
,因为字段类型被完全丢弃)。