验证器
除了 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] """
-
请注意
value
的类型提示使用了Any
。前置验证器接受原始输入,可以是任何类型。 -
请注意,您可能需要检查其他通常可以成功验证
list
类型的序列类型(例如元组)。前置验证器为您提供了更大的灵活性,但您必须考虑每种可能的情况。 -
无论我们的
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] """
-
请注意
value
的类型提示使用了Any
。前置验证器接受原始输入,可以是任何类型。 -
请注意,您可能需要检查其他通常可以成功验证
list
类型的序列类型(例如元组)。前置验证器为您提供了更大的灵活性,但您必须考虑每种可能的情况。 -
无论我们的
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'
- 尽管
'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 和其他验证器处理输入之前或之后运行代码,或者您可以通过提前返回值或引发错误来立即终止验证。
此类验证器必须使用一个强制性的额外
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)!
- 正如注解模式文档中提到的,我们还可以为注解的特定部分使用验证器(在本例中,验证应用于列表项,但不应用于整个列表)。
通过查看字段注解,也更容易理解哪些验证器应用于类型。
使用装饰器模式¶
使用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
- 请注意
data
的类型提示使用了Any
。前置验证器接受原始输入,可以是任何类型。 - 大多数情况下,输入数据将是一个字典(例如,当调用
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 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]
- 请注意,跳过了第二个项目的验证。如果它的类型错误,则会在序列化期间发出警告。
-
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
,因为字段类型被完全丢弃)。