FEATURED · 精选文章

Pydantic 校验器完全指南:Field Validator 与 Model Validator 的四种模式、执行顺序与实战用法

发布时间 / 2026/9/10 18:44:24
来源 / 创域科博编辑部
栏目 / 资讯中心
Pydantic 校验器完全指南:Field Validator 与 Model Validator 的四种模式、执行顺序与实战用法 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(number1) except ValidationError as err: print(err) 1 validation error for Model number Value error, 1 is not an even number [typevalue_error, input_value1, input_typeint] 装饰器模式示例——使用field_validator装饰器实现同样的逻辑from pydantic import BaseModel, ValidationError, field_validator class Model(BaseModel): number: int field_validator(number, modeafter) # 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(number1) except ValidationError as err: print(err) 1 validation error for Model number Value error, 1 is not an even number [typevalue_error, input_value1, input_typeint] 修改值的示例——不抛异常直接对校验值做变更coercion/mutationfrom 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(number2)) # number4before 校验器字段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(numbers2)) # numbers[2] try: Model(numbersstr) 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 [typeint_parsing, input_valuestr, input_typestr] 装饰器模式示例from typing import Any from pydantic import BaseModel, ValidationError, field_validator class Model(BaseModel): numbers: list[int] field_validator(numbers, modebefore) classmethod def ensure_list(cls, value: Any) - Any: # 原始输入可能是任意类型 if not isinstance(value, list): return [value] else: return value print(Model(numbers2)) # numbers[2] try: Model(numbersstr) 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 [typeint_parsing, input_valuestr, input_typestr] 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(number4)) # number8 print(Model(numberinvalid)) # 尽管 invalid 本不应通过 int 类型校验Pydantic 仍接受该输入 # numberinvalid装饰器模式示例from typing import Any from pydantic import BaseModel, field_validator class Model(BaseModel): number: int field_validator(number, modeplain) classmethod def val_number(cls, value: Any) - Any: if isinstance(value, int): return value * 2 else: return value print(Model(number4)) # number8 print(Model(numberinvalid)) # 尽管 invalid 本不应通过 int 类型校验Pydantic 仍接受该输入 # numberinvalidwrap 校验器字段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_length5), WrapValidator(truncate)] print(Model(my_stringabcde)) # my_stringabcde print(Model(my_stringabcdef)) # my_stringabcde装饰器模式示例from typing import Any, Annotated from pydantic import BaseModel, Field, ValidationError, ValidatorFunctionWrapHandler, field_validator class Model(BaseModel): my_string: Annotated[str, Field(max_length5)] field_validator(my_string, modewrap) 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_stringabcde)) # my_stringabcde print(Model(my_stringabcdef)) # my_stringabcde从源码看WrapValidator会通过_inspect_validator(self.func, modewrap, typefield)检查函数签名是否带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, modebefore) 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(modeafter) def check_passwords_match(self) - Self: if self.password ! self.password_repeat: raise ValueError(Passwords do not match) return self从源码看对modeafter使用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(modebefore) classmethod def check_card_number_not_present(cls, data: Any) - Any: # 注意data 使用 Anybefore 校验器接收原始输入 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(modewrap) 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, modeafter) 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(x42 * 2) except ValidationError as e: print(e) 1 validation error for Model x 84 is the answer! [typethe_answer_error, input_value84, input_typeint] 自定义错误类型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, modeafter) 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, modeafter) 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)) # 无上下文 # textThis is an example document print(Model.model_validate(data, context{stopwords: [this, is, an]})) # textexample 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, defaultNone) 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_instanceself, 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_number2)) # my_number2 with init_context({multiplier: 3}): print(Model(my_number2)) # my_number6 print(Model(my_number2)) # my_number2 校验器的执行顺序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 typesPydantic 提供了一些特殊工具用于定制校验行为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 [typeis_instance_of, input_valueApple, input_typestr] 从源码看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 fMyCls(a{self.a}) class ValModel(BaseModel): a: int ta TypeAdapter( Annotated[MyCls, ValidateAs(ValModel, lambda v: MyCls(av.a))] ) print(ta.validate_python({a: 1})) # MyCls(a1)从源码看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(nameNone)) # namedefault_nameJSON Schema 与字段校验器的联动当使用before、plain或wrap字段校验器时可接受的输入类型可能与字段注解不同。考虑下面的例子from typing import Any from pydantic import BaseModel, field_validator class Model(BaseModel): value: str field_validator(value, modebefore) classmethod def cast_ints(cls, value: Any) - Any: if isinstance(value, int): return str(value) else: return value print(Model(valuea)) # valuea print(Model(value1)) # value1value的类型提示是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, modebefore, json_schema_input_typeint | 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.pyAfterValidator对应的modeafter则不支持该参数。结语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),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻