Validators
除了 Pydantic 的内置验证功能之外,您还可以利用字段级别和模型级别的自定义验证器来强制执行更复杂的约束并确保数据的完整性。
Tip
想要快速跳转到相关的验证器部分?
字段验证器¶
API 文档
pydantic.functional_validators.WrapValidator
pydantic.functional_validators.PlainValidator
pydantic.functional_validators.BeforeValidator
pydantic.functional_validators.AfterValidator
pydantic.functional_validators.field_validator
在最简单的形式中,字段验证器是一个可调用对象,它接收要验证的值作为参数并返回验证后的值。该可调用对象可以执行特定条件的检查(参见引发验证错误)并对验证后的值进行更改(强制转换或修改)。
可以使用四种不同类型的验证器。它们都可以使用注解模式或使用 field_validator() 装饰器来定义,应用于类方法:
-
After 验证器:在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=4from 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'是装饰器的默认模式,可以省略。
-
Before 验证器:在 Pydantic 的内部解析和验证之前运行(例如,将
str强制转换为int)。 这些比 after 验证器更灵活,但它们也必须处理原始输入, 理论上可以是任何任意对象。如果您稍后在验证器函数中引发验证错误, 还应避免直接修改值,因为如果使用联合类型,修改后的值可能会传递给其他验证器。然后,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的类型提示。Before 验证器接收原始输入, 可以是任何东西。 -
注意您可能想要检查其他序列类型(例如元组),这些类型通常可以成功验证为
list类型。 Before 验证器给您更多灵活性,但您必须考虑所有可能的情况。 -
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的类型提示。Before 验证器接收原始输入, 可以是任何东西。 -
注意您可能想要检查其他序列类型(例如元组),这些类型通常可以成功验证为
list类型。 Before 验证器给您更多灵活性,但您必须考虑所有可能的情况。 -
Pydantic 仍然对
int类型执行验证,无论我们的ensure_list验证器是否对原始输入类型进行了操作。
-
-
Plain 验证器:行为类似于 before 验证器,但它们在返回后立即终止验证, 因此不会调用其他验证器,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 接受了输入。
- 虽然
-
Wrap 验证器:是所有验证器中最灵活的。您可以在 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() 装饰器对整个模型的数据执行验证。
可以使用三种不同类型的模型验证器:
-
After 验证器:在整个模型验证之后运行。因此,它们被定义为实例方法, 可以看作是后初始化钩子。重要说明:应返回验证后的实例。
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 -
Before 验证器:在模型实例化之前运行。这些比 after 验证器更灵活, 但它们也必须处理原始输入,理论上可以是任何任意对象。如果您稍后在验证器函数中 引发验证错误,还应避免直接修改值,因为如果使用 联合类型,修改后的值可能会传递给其他验证器。
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的类型提示。Before 验证器接收原始输入, 可以是任何东西。 - 大多数情况下,输入数据将是一个字典(例如,当调用
UserModel(username='...')时)。然而, 情况并非总是如此。例如,如果设置了from_attributes配置值,您可能会收到一个任意类实例作为data参数。
- 注意使用
-
Wrap 验证器:是所有验证器中最灵活的。您可以在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 属性访问已验证的数据。
以下是一个可以作为 after 模型验证器示例替代方案的示例:
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
Warning
由于验证是按照字段定义的顺序执行的,您必须
确保不访问尚未验证的字段。例如,在上面的代码中,
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)) # 无上下文
#> 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
验证器排序¶
使用注解模式时,验证器的应用顺序定义如下: before 和 wrap 验证器从右到左运行, 然后 after 验证器从左到右运行:
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 和字段验证器¶
使用 before、plain 或 wrap 字段验证器时,接受的输入类型可能与字段注解不同。
考虑以下示例:
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 将使用字段类型(除非您使用的是 plain 验证器,
在这种情况下,json_schema_input_type默认为 Any,因为字段类型被完全丢弃)。