Pydantic 校验器完全指南:Field Validator 与 Model Validator 的四种模式、执行顺序与实战用法
【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic
本文以 Pydantic 的校验器(Validators)体系为核心,系统讲解字段级(Field)与模型级(Model)校验器的before、after、plain、wrap四种模式,以及 Annotated 模式与@field_validator/@model_validator装饰器两种定义方式。读完本文,你将能够编写可复用的复杂约束校验、跨字段校验、带上下文的校验逻辑,并理解校验器的执行顺序、错误抛出方式与 JSON Schema 联动等底层原理。本文内容以 docs/concepts/validators.md 为骨架,并结合 pydantic/functional_validators.py 源码与 tests/test_validators.py 测试进行印证。
在 Pydantic 内置的字段约束校验能力之外,你可以在字段级和模型级使用自定义校验器(custom validators),以强制执行更复杂的约束,并保证数据的完整性。
字段级校验器(Field validators)
字段级校验器的 API 文档可参考pydantic.functional_validators模块中的WrapValidator、PlainValidator、BeforeValidator、AfterValidator与field_validator。
最简单形式的字段校验器,是一个接收待校验值作为参数、并返回校验后值的可调用对象。该可调用对象既可以针对特定条件做检查(详见下文抛出校验错误),也可以对校验值做出修改(类型强转或变更,即 coercion 或 mutation)。
字段校验器共有四种模式,既可以借助 Annotated 模式(annotated pattern)定义,也可以通过@field_validator装饰器应用于类方法:
| 模式 | 运行时机 | 典型特征 |
|---|---|---|
after | 在 Pydantic 内部校验之后运行 | 更类型安全,更易实现 |
before | 在 Pydantic 内部解析与校验之前运行 | 更灵活,但需处理任意原始输入 |
plain | 类似 before,但返回后立即终止校验 | 不再执行其他校验器,Pydantic 也不再做内部校验 |
wrap | 可在 Pydantic 及其他校验器处理前后运行代码 | 最灵活,可提前返回或抛错终止校验 |
after 校验器(字段)
after校验器在 Pydantic 的内部校验之后运行,一般来说更类型安全、更容易实现。
Annotated 模式示例——执行校验检查并原样返回值:
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 # 注意:必须返回校验后的值 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') # 'after' 是装饰器默认模式,可以省略 @classmethod def is_even(cls, value: int) -> int: if value % 2 == 1: raise ValueError(f'{value} is not an even number') return value # 注意:必须返回校验后的值 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] """修改值的示例——不抛异常,直接对校验值做变更(coercion/mutation):
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=4before 校验器(字段)
before校验器在 Pydantic 的内部解析和校验(例如把str强转为int)之前运行。它比after校验器更灵活,但也必须处理原始输入——理论上它可以是任意对象。另外请注意:如果在校验器后续还要抛出校验错误,应避免直接修改(mutate)值,因为当使用联合类型 unions 时,被修改过的值可能会被传递给其他校验器。
该可调用对象返回的值,随后会由 Pydantic 针对声明的类型注解继续做校验。
Annotated 模式示例——把非列表输入包装成列表:
from typing import Annotated, Any from pydantic import BaseModel, BeforeValidator, ValidationError def ensure_list(value: Any) -> Any: # 注意:value 使用 Any 类型提示,因为 before 校验器接收的是原始输入 if not isinstance(value, list): # 可能还需要考虑 tuple 等其他序列类型 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) # Pydantic 仍然会对 int 类型做校验,无论 ensure_list 对原始输入做了什么操作 """ 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] """装饰器模式示例:
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: # 原始输入,可能是任意类型 if not isinstance(value, list): return [value] else: return value print(Model(numbers=2)) #> numbers=[2] try: Model(numbers='str') except ValidationError as err: print(err) # 即便 ensure_list 处理过原始输入,Pydantic 仍会对 int 类型执行校验 """ 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] """plain 校验器(字段)
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')) # 尽管 'invalid' 本不应通过 int 类型校验,Pydantic 仍接受该输入 #> number='invalid'装饰器模式示例:
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')) # 尽管 'invalid' 本不应通过 int 类型校验,Pydantic 仍接受该输入 #> number='invalid'wrap 校验器(字段)
wrap校验器是四种模式中最灵活的:你可以在 Pydantic 及其他校验器处理输入之前或之后运行代码,也可以立即终止校验——要么提前返回值,要么抛出错误。
这类校验器必须定义一个额外的、强制的 handler 参数:handler 是一个以待校验值为参数的可调用对象。内部实现上,这个 handler 会把值的校验委托给 Pydantic 完成。你可以自由决定是否用try..except包裹对 handler 的调用,甚至可以不调用它。
Annotated 模式示例——在string_too_long错误时截断字符串后重试:
from typing import Any, 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, 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'从源码看,WrapValidator会通过_inspect_validator(self.func, mode='wrap', type='field')检查函数签名是否带info参数,并据此生成with_info_wrap_validator_function或no_info_wrap_validator_function的 core schema(参见 pydantic/functional_validators.py)。tests/test_validators.py中的test_annotated_validator_wrap也验证了这一模式的行为。
!!! note "默认值不参与校验" 如字段文档所述,字段的默认值默认不会被校验,因此自定义校验器也不会应用于默认值,除非显式配置为校验默认值。
两种定义模式如何选择
两种方式可以达到同样的效果,但各有不同的收益。
使用 Annotated 模式
Annotated 模式(annotated pattern)的一大关键优势是让校验器可复用:
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] # 校验作用于列表项,而非整个列表如 annotated pattern 文档所述,我们还可以针对注解的特定部分使用校验器(本例中校验应用于列表项,而非整个列表)。此外,通过直接查看字段注解,就能更容易地理解该类型被应用了哪些校验器。
使用装饰器模式
@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()关于装饰器用法,还有几点补充说明:
- 如果希望校验器应用于所有字段(包括子类中定义的字段),可以把
'*'作为字段名参数传入。 - 默认情况下,装饰器会确保提供的字段名定义在模型上。如果希望在类创建期间禁用这一检查,可以给
check_fields参数传False。这在字段校验器定义在基类、而字段预期存在于子类时非常有用。 - 从源码看,
field_validator会自动把函数包装为classmethod(_decorators.ensure_classmethod_based_on_signature),若直接作用于实例方法或缺少字段名参数,会抛出PydanticUserError(参见 pydantic/functional_validators.py),对应错误码包括decorator-missing-arguments、decorator-invalid-fields、validator-instance-method等。
模型级校验器(Model validators)
模型级校验器的 API 文档可参考pydantic.functional_validators.model_validator。校验还可以通过@model_validator装饰器在整个模型的数据上执行。
模型校验器共有三种模式:
| 模式 | 运行时机 | 定义形态 |
|---|---|---|
after | 整个模型校验完成后运行 | 实例方法,可视为后初始化钩子,必须返回校验后的实例 |
before | 模型实例化之前运行 | 类方法,需处理任意原始输入 |
wrap | 可在 Pydantic 及其他校验器处理前后运行 | 最灵活,可提前返回或抛错 |
after 校验器(模型)
after模型校验器在整个模型校验完成后运行,因此定义为实例方法,可以视作后初始化钩子(post-initialization hooks)。重要提示:必须返回校验后的实例。
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从源码看,对mode='after'使用classmethod在 v2.12 起已标记为弃用并发出PydanticDeprecatedSince212警告,官方建议改为实例方法(参见 pydantic/functional_validators.py)。
before 校验器(模型)
before模型校验器在模型实例化之前运行。它比after校验器更灵活,但也必须处理原始输入——理论上可以是任意对象。同样,如果后续要抛出校验错误,应避免直接修改值,因为使用联合类型 unions 时修改后的值可能被传递给其他校验器。
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: # 注意:data 使用 Any,before 校验器接收原始输入 if isinstance(data, dict): # 大多数情况下输入是字典(如 UserModel(username='...')),但并非总是如此 if 'card_number' in data: raise ValueError("'card_number' should not be included") return data注意:大多数时候输入数据是一个字典,但并非总是如此。例如设置了from_attributes配置时,data参数收到的可能是任意类实例。
wrap 校验器(模型)
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从源码看,model_validator接受mode为'wrap'、'before'、'after'之一(参见 pydantic/functional_validators.py),并定义了ModelWrapValidatorHandler、ModelBeforeValidator、ModelAfterValidator等协议类型来约束函数签名。
!!! note "关于继承" 定义在基类中的模型校验器,会在子类实例的校验过程中被调用。 如果在子类中覆盖(override)该模型校验器,则会覆盖基类的校验器,因此只会调用子类版本。
抛出校验错误(Raising validation errors)
在校验器内部抛出校验错误,可以使用三种异常类型:
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] """自定义错误类型the_answer_error会出现在type字段中,便于程序化处理。当校验器在生产环境中拒绝数据时,Logfire 可以把被拒绝的值记录在其结构化错误中,方便你定位是哪条规则被违反。
校验信息(Validation info)
字段校验器和模型校验器的可调用对象(在所有模式下)都可以可选地接收一个额外的ValidationInfo参数,提供有用的附加信息:
- 已经校验过的数据(validation data)
- 用户自定义的上下文(validation context)
- 当前的校验模式(
mode属性):'python'、'json'或'strings'(参见验证数据) - 当前字段名,若使用的是字段校验器(
field_name属性)
校验数据(Validation data)
对于字段校验器,可以通过ValidationInfo.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之后,因此它的校验值此时还不可用。
另外,data属性对于模型校验器而言是None。
校验上下文(Validation context)
你可以向校验方法传入一个上下文对象,并在校验器函数内通过ValidationInfo.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'类似地,你也可以为序列化使用上下文。
??? note "直接实例化模型时提供上下文的方案" 目前无法在直接实例化模型时(即调用Model(...))提供上下文。可以通过ContextVar与自定义__init__方法绕过这一限制:
```python 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 ```校验器的执行顺序(Ordering of validators)
当使用 Annotated 模式时,校验器的应用顺序定义如下: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), ]内部实现上,通过装饰器定义的校验器会被转换为等价的 Annotated 形式,并追加到字段已有 metadata 的最后,因此同样的顺序逻辑同样适用。tests/test_validators.py中的test_annotated_validator_runs_before_field_validators也验证了注解校验器与装饰器校验器之间的相对顺序。
特殊类型工具(Special types)
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] """从源码看,
InstanceOf通过__get_pydantic_core_schema__生成core_schema.is_instance_schema,并优先尝试生成"标准"schema 用于 JSON 加载时的校验,否则退化为仅支持 python 校验的 schema(参见 pydantic/functional_validators.py)。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]) # 第二个元素的校验被跳过 print(m) #> names=['foo', 123]注意:第二个元素
123的校验被跳过了。如果它类型错误,在序列化时会发出警告。源码中SkipValidation会把校验 schema 转换为any_schema,因此该注解通常应作为类型上应用的最后一个注解(参见 pydantic/functional_validators.py)。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)从源码看,
ValidateAs(from_type, instantiation_hook)会先针对from_type生成校验 schema,再把instantiation_hook作为after校验器接入,从而把校验后的值构造成自定义类型(参见 pydantic/functional_validators.py)。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 中声明正确的输入类型,可以提供json_schema_input_type参数:
from typing import Any from pydantic import BaseModel, field_validator class Model(BaseModel): value: str @field_validator('value', mode='before', json_schema_input_type=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,因为字段类型被完全丢弃了。从源码看,json_schema_input_type只能在mode为'before'、'plain'或'wrap'时指定,否则会抛出PydanticUserError(错误码validator-input-type,参见 pydantic/functional_validators.py);AfterValidator对应的mode='after'则不支持该参数。
结语
Pydantic 的校验器体系为数据完整性提供了从字段到模型的全方位保障:after保证类型安全、before处理原始输入、plain完全接管校验、wrap提供最大自由度;Annotated 模式与装饰器模式则分别服务于"复用"与"多字段应用"两种场景。结合ValidationInfo提供的已校验数据与用户上下文、PydanticCustomError的定制错误、以及json_schema_input_type对 JSON Schema 的联动修正,你可以构建出既严谨又灵活的校验逻辑。想要进一步验证行为,可以阅读 tests/test_validators.py 中的test_annotated_validator_after、test_annotated_validator_before、test_annotated_validator_plain、test_annotated_validator_wrap等测试用例。
【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考