Pydantic 验证器完全指南
Pydantic v2 中的数据验证机制
目录
概述
Pydantic v2 提供两种主要的验证器:
| 验证器 |
作用范围 |
典型场景 |
field_validator |
单个或多个字段 |
格式验证、类型转换后检查 |
model_validator |
整个模型 |
字段间依赖关系、组合验证 |
field_validator
基本语法
from pydantic import BaseModel, field_validator
class User(BaseModel):
age: int
@field_validator('age')
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('年龄不能为负')
return v
参数说明
| 参数 |
类型 |
默认值 |
说明 |
*fields |
str |
- |
要验证的字段名(必填,可多个) |
mode |
Literal['before', 'after', 'wrap'] |
'after' |
验证时机 |
check_fields |
bool |
True |
是否检查字段存在性 |
mode 参数详解
mode='after'(默认)
@field_validator('age', mode='after')
def validate_age(cls, v: int) -> int:
# 此时 v 已经完成类型转换
if v < 0:
raise ValueError('年龄不能为负')
return v
- 触发时机:类型转换之后
- 值的类型:已转换为目标类型
- 典型用途:业务规则验证
mode='before'
@field_validator('age', mode='before')
def preprocess_age(cls, v):
# 此时 v 是原始输入值
print(f'原始输入: {v}, 类型: {type(v)}')
# 可以在类型转换前进行处理
if isinstance(v, str):
return v.strip()
return v
- 触发时机:类型转换之前
- 值的类型:原始输入类型
- 典型用途:数据预处理、清理
mode='wrap'
@field_validator('age', mode='wrap')
def wrap_age_validation(cls, v, handler):
# handler 执行后续验证流程
result = handler(v)
# 可以在验证前后添加逻辑
return result
- 触发时机:包裹整个验证过程
- 可访问:handler(下一个验证器)
- 典型用途:拦截/修改验证流程、日志记录
多字段验证
@field_validator('password', 'confirm_password')
def validate_passwords(cls, v: str, info: ValidationInfo) -> str:
# info.data 提供其他字段的值
if len(v) < 8:
raise ValueError('密码长度至少8位')
return v
常见使用场景
from pydantic import BaseModel, field_validator
class Product(BaseModel):
name: str
price: float
email: str
stock: int
@field_validator('name')
def name_not_empty(cls, v: str) -> str:
"""验证名称非空"""
if not v.strip():
raise ValueError('名称不能为空')
return v.strip()
@field_validator('price')
def price_positive(cls, v: float) -> float:
"""验证价格为正数"""
if v <= 0:
raise ValueError('价格必须大于0')
return round(v, 2)
@field_validator('email')
def email_format(cls, v: str) -> str:
"""验证邮箱格式"""
if '@' not in v:
raise ValueError('邮箱格式不正确')
return v.lower()
@field_validator('stock')
def stock_non_negative(cls, v: int) -> int:
"""验证库存非负"""
if v < 0:
raise ValueError('库存不能为负')
return v
在 BaseSettings 中的使用
from pydantic_settings import BaseSettings
from pydantic import field_validator
class Settings(BaseSettings):
DEBUG: bool = False
VERSION: str = "1.0.0"
@field_validator("VERSION", mode="before", check_fields=False)
def validate_version(cls, v: str) -> str:
"""在读取环境变量前验证版本号"""
if not v:
return "1.0.0"
return v
model_validator
基本语法
from pydantic import BaseModel, model_validator
class User(BaseModel):
password: str
confirm_password: str
@model_validator(mode='after')
def passwords_match(self):
if self.password != self.confirm_password:
raise ValueError('密码不匹配')
return self
参数说明
| 参数 |
类型 |
默认值 |
说明 |
mode |
Literal['before', 'after', 'wrap'] |
'after' |
验证时机 |
mode 参数详解
mode='after'(最常用)
@model_validator(mode='after')
def validate_relationships(self):
# 所有字段已完成验证
# 可以访问所有字段并进行关系验证
if self.start_date > self.end_date:
raise ValueError('开始日期不能晚于结束日期')
return self
- 触发时机:所有字段验证完成后
- 可访问:完整的模型实例
- 典型用途:字段间关系验证
mode='before'
@model_validator(mode='before')
@classmethod
def preprocess_input(cls, data: dict) -> dict:
# 在任何字段验证前处理原始输入
data = {k: v for k, v in data.items() if v is not None}
return data
- 触发时机:任何字段验证之前
- 输入:原始字典数据
- 返回:修改后的字典
- 典型用途:全局预处理
mode='wrap'
@model_validator(mode='wrap')
@classmethod
def wrap_validation(cls, data, handler):
# handler 执行整个验证流程
result = handler(data)
# 可以在验证前后添加逻辑
return result
- 触发时机:包裹整个模型验证
- 可访问:handler(验证处理器)
- 典型用途:拦截验证流程、日志记录
使用场景
场景 1:字段依赖验证(最常见)
from datetime import date
class BookingRequest(BaseModel):
start_date: date
end_date: date
@model_validator(mode='after')
def validate_dates(self):
if self.end_date <= self.start_date:
raise ValueError('结束日期必须晚于开始日期')
return self
场景 2:互斥字段验证
class DiscountRequest(BaseModel):
percentage: float | None = None
fixed_amount: float | None = None
@model_validator(mode='after')
def validate_discount_type(self):
if self.percentage and self.fixed_amount:
raise ValueError('不能同时使用百分比和固定金额折扣')
if not self.percentage and not self.fixed_amount:
raise ValueError('必须指定一种折扣类型')
return self
场景 3:条件必填字段
class UserRegistration(BaseModel):
user_type: str # 'individual' or 'company'
personal_id: str | None = None
company_code: str | None = None
@model_validator(mode='after')
def validate_required_fields(self):
if self.user_type == 'individual' and not self.personal_id:
raise ValueError('个人用户必须提供身份证号')
if self.user_type == 'company' and not self.company_code:
raise ValueError('企业用户必须提供企业代码')
return self
场景 4:范围验证(字段间的约束)
class Payment(BaseModel):
amount: float
min_amount: float = 10.0
max_amount: float = 10000.0
@model_validator(mode='after')
def validate_amount_range(self):
if self.amount < self.min_amount:
raise ValueError(
f'金额不能低于最低限额 {self.min_amount}'
)
if self.amount > self.max_amount:
raise ValueError(
f'金额不能超过最高限额 {self.max_amount}'
)
return self
场景 5:动态默认值计算
class UserProfile(BaseModel):
first_name: str
last_name: str
full_name: str | None = None
@model_validator(mode='after')
def generate_full_name(self):
if not self.full_name:
# 动态计算并设置默认值
self.full_name = f"{self.first_name} {self.last_name}"
return self
场景 6:mode='before' - 预处理
class ProductModel(BaseModel):
name: str
price: float
description: str | None = None
@model_validator(mode='before')
@classmethod
def normalize_input(cls, data: dict) -> dict:
# 清理空字符串
return {
k: (v if v != '' else None)
for k, v in data.items()
}
场景 7:mode='wrap' - 条件验证
class ConfigurableModel(BaseModel):
value: int
strict_mode: bool = False
@model_validator(mode='wrap')
@classmethod
def conditional_validation(cls, value, handler):
# handler 执行后续验证
model = handler(value)
# 根据配置决定是否执行额外验证
if model.strict_mode and model.value < 0:
raise ValueError('严格模式下不允许负数')
return model
源码解析
源码位置
Pydantic 的验证器实现位于核心源码文件中:
# 文件路径
pydantic/functional_validators.py
field_validator 源码分析
函数签名(简化版)
def field_validator(
field: str, # 第一个字段名
/, # 位置参数分隔符
*fields: str, # 额外的字段名
mode: FieldValidatorModes = 'after', # 验证模式
check_fields: bool | None = None, # 是否检查字段存在
json_schema_input_type: Any = PydanticUndefined, # JSON Schema 输入类型
) -> Callable[[Any], Any]:
关键源码逻辑
# 1. 参数验证
if isinstance(field, FunctionType):
raise PydanticUserError(
'`@field_validator` should be used with fields and keyword arguments, not bare.'
)
# 2. 模式与 json_schema_input_type 兼容性检查
if mode not in ('before', 'plain', 'wrap') and json_schema_input_type is not PydanticUndefined:
raise PydanticUserError(
f"`json_schema_input_type` can't be used when mode is set to {mode!r}"
)
# 3. 字段类型验证
fields = field, *fields
if not all(isinstance(field, str) for field in fields):
raise PydanticUserError('`@field_validator` fields should be passed as separate string args.')
# 4. 装饰器工厂函数
def dec(f: Callable[..., Any]) -> _decorators.PydanticDescriptorProxy[Any]:
# 检查是否为实例方法
if _decorators.is_instance_method_from_sig(f):
raise PydanticUserError('`@field_validator` cannot be applied to instance methods')
# 自动应用 @classmethod 装饰器
f = _decorators.ensure_classmethod_based_on_signature(f)
# 创建装饰器信息对象
dec_info = _decorators.FieldValidatorDecoratorInfo(
fields=fields,
mode=mode,
check_fields=check_fields,
json_schema_input_type=json_schema_input_type
)
return _decorators.PydanticDescriptorProxy(f, dec_info)
return dec
设计模式分析
# 1. 装饰器工厂模式
@field_validator('email') # 返回装饰器函数
def validate_email(cls, v): # 被装饰的验证函数
return v
# 2. 代理模式
class PydanticDescriptorProxy:
"""代理对象,延迟绑定到类"""
def __init__(self, func, info):
self.func = func
self.info = info
# 3. 信息封装
@dataclasses.dataclass(frozen=True)
class FieldValidatorDecoratorInfo:
fields: tuple[str, ...]
mode: FieldValidatorModes
check_fields: bool | None
json_schema_input_type: Any
源码中的关键机制
# 1. 自动类方法转换
# _decorators.py 中的实现
def ensure_classmethod_based_on_signature(f):
"""根据函数签名自动应用 @classmethod"""
if isinstance(f, (classmethod, staticmethod)):
return f
# 检查第一个参数是否为 'cls' 或类似名称
# 如果是,自动包装为 classmethod
return classmethod(f)
# 2. 字段存在性检查
if check_fields is not False: # 默认为 True
# 在模型收集阶段验证字段是否存在
# 如果字段不存在,抛出 PydanticUserError
model_validator 源码分析
函数签名(简化版)
def model_validator(
*,
mode: Literal['wrap', 'before', 'after'], # 必填参数
) -> Any:
关键源码逻辑
def model_validator(
*,
mode: Literal['wrap', 'before', 'after'],
) -> Any:
"""装饰模型方法以进行验证"""
def dec(f: Any) -> _decorators.PydanticDescriptorProxy[Any]:
# 关键区别:after 模式保持实例方法
if mode != 'after':
# before 和 wrap 模式自动应用 @classmethod
f = _decorators.ensure_classmethod_based_on_signature(f)
# 创建模型验证器装饰器信息
dec_info = _decorators.ModelValidatorDecoratorInfo(mode=mode)
return _decorators.PydanticDescriptorProxy(f, dec_info)
return dec
设计模式分析
# 1. 重载模式(类型检查支持)
@overload
def model_validator(*, mode: Literal['wrap']) -> ...: ...
@overload
def model_validator(*, mode: Literal['before']) -> ...: ...
@overload
def model_validator(*, mode: Literal['after']) -> ...: ...
# 2. 协议定义(类型提示)
class ModelWrapValidatorHandler(Protocol[_ModelTypeCo]):
"""@model_validator wrap 模式的 handler 类型"""
def __call__(
self,
value: Any,
outer_location: str | int | None = None,
/,
) -> _ModelTypeCo: ...
# 3. 不同模式的验证器类型
ModelAfterValidator = Callable[[_ModelType, ValidationInfo], _ModelType]
ModelBeforeValidator = Callable[[cls, value, info], Any]
ModelWrapValidator = Callable[[cls, value, handler, info], _ModelType]
源码中的关键机制
# 1. 模式特定处理
if mode != 'after':
# before 和 wrap 模式:必须是类方法或静态方法
f = _decorators.ensure_classmethod_based_on_signature(f)
# else:
# after 模式:保持实例方法(可以访问 self)
# 2. 装饰器信息结构
@dataclasses.dataclass(frozen=True)
class ModelValidatorDecoratorInfo:
mode: Literal['wrap', 'before', 'after']
核心架构对比
验证流程架构
# 1. 验证器注册(类定义时)
class BaseModel:
# 收集所有 @field_validator 和 @model_validator
# 存储在 __pydantic_core_schema__ 中
# 2. 验证执行(实例化时)
def validate_python(cls, data):
# 执行顺序:
# model_validator(mode='before')
# field_validator(mode='before') # 每个字段
# 类型转换
# field_validator(mode='after') # 每个字段
# model_validator(mode='after')
源码中的数据流
# 1. 用户输入
data = {'email': 'test@example.com'}
# 2. before 模式验证器
# 接收原始字典,返回处理后的字典
data = model_validator_before(cls, data)
# 3. 字段级验证
for field_name, field_value in data.items():
# before 验证器
value = field_validator_before(cls, field_value)
# 类型转换(pydantic-core Rust 实现)
value = pydantic_core.validate(value, field_type)
# after 验证器
value = field_validator_after(cls, value)
# 4. after 模式验证器
# 接收完全验证的模型实例
model = model_validator_after(model_instance)
# 5. 返回最终模型
return model
内部数据结构
FieldValidatorDecoratorInfo
@dataclasses.dataclass(frozen=True)
class FieldValidatorDecoratorInfo:
"""字段验证器的装饰器信息"""
fields: tuple[str, ...] # 要验证的字段名
mode: FieldValidatorModes # 'before' | 'after' | 'wrap' | 'plain'
check_fields: bool | None # 是否检查字段存在
json_schema_input_type: Any # JSON Schema 输入类型
ModelValidatorDecoratorInfo
@dataclasses.dataclass(frozen=True)
class ModelValidatorDecoratorInfo:
"""模型验证器的装饰器信息"""
mode: Literal['wrap', 'before', 'after'] # 验证模式
PydanticDescriptorProxy
class PydanticDescriptorProxy:
"""
Pydantic 装饰器的代理对象
延迟绑定机制:
1. 在类定义时创建代理对象
2. 在类收集阶段解析为实际的验证器
3. 在验证执行时调用用户定义的函数
"""
def __init__(self, func, info):
self.func = func # 用户定义的验证函数
self.info = info # 装饰器信息(字段、模式等)
源码级最佳实践
1. 利用自动类方法转换
# 源码会自动识别 'cls' 参数并应用 @classmethod
@field_validator('email')
def validate_email(cls, v): # 不需要手动写 @classmethod
return v
# 等价于源码处理后的:
@field_validator('email')
@classmethod
def validate_email(cls, v):
return v
2. 理解 check_fields 参数
# check_fields=True(默认):字段不存在时报错
@field_validator('nonexistent_field')
def validate(cls, v):
return v
# PydanticUserError: Fields specified in field_validator do not exist on the model
# check_fields=False:允许验证可能不存在的字段
@field_validator('optional_field', check_fields=False)
def validate(cls, v):
return v
3. 模式特定的签名要求
# before 模式:返回处理后的字典
@model_validator(mode='before')
@classmethod
def preprocess(cls, data: dict) -> dict:
data['timestamp'] = datetime.now()
return data
# after 模式:返回模型实例
@model_validator(mode='after')
def validate(self):
if self.field1 < self.field2:
raise ValueError('field1 must be >= field2')
return self
# wrap 模式:返回处理后的模型
@model_validator(mode='wrap')
@classmethod
def wrap_validate(cls, data: Any, handler: ModelWrapValidatorHandler) -> ModelType:
# handler 执行后续验证
model = handler(data)
return model
性能考虑
源码中的性能优化
# 1. frozen dataclass:不可变对象,线程安全
@dataclasses.dataclass(frozen=True)
class FieldValidatorDecoratorInfo:
...
# 2. 延迟绑定:只在需要时解析验证器
class PydanticDescriptorProxy:
def __set_name__(self, owner, name):
# 在类收集阶段绑定
self._owner = owner
self._name = name
# 3. Rust 核心:类型转换由 pydantic-core 实现
# field_validator 只负责业务逻辑验证
错误处理机制
# 源码中的错误类型
class PydanticUserError(Exception):
"""用户使用错误"""
# code:
# - 'validator-no-fields': @field_validator 未指定字段
# - 'validator-invalid-fields': 字段名不是字符串
# - 'validator-instance-method': 应用于实例方法
# - 'validator-input-type': json_schema_input_type 使用不当
# 错误示例
@field_validator() # 缺少字段名
def validate(cls, v):
return v
# PydanticUserError: @field_validator should be used with fields...
扩展性设计
# 1. Protocol 类型提示
class ModelAfterValidator(Protocol[_ModelType]):
"""允许自定义验证器类型"""
def __call__(self, model: _ModelType, info: ValidationInfo) -> _ModelType: ...
# 2. Annotated 类型支持
from typing import Annotated
from pydantic import AfterValidator
# 使用 Annotated 组合验证器
Email = Annotated[str, AfterValidator(lambda v: v.lower())]
# 3. 可组合的验证器
def double(v: int) -> int:
return v * 2
def add_one(v: int) -> int:
return v + 1
MyInt = Annotated[int, AfterValidator(double), AfterValidator(add_one)]
# 验证顺序:double -> add_one
对比总结
功能对比
| 特性 |
field_validator |
model_validator |
| 验证范围 |
单个或多个指定字段 |
整个模型 |
| 访问其他字段 |
通过 info.data(有限) |
直接访问所有字段 |
| 执行顺序 |
各字段独立 |
所有字段验证后 |
| 修改字段 |
只能修改当前字段 |
可以修改任何字段 |
| 返回值 |
修改后的字段值 |
模型实例或字典 |
选择建议
# ✅ 使用 field_validator 的情况
class User(BaseModel):
email: str
@field_validator('email')
def validate_email(cls, v):
# 单字段格式验证
if '@' not in v:
raise ValueError('邮箱格式错误')
return v
# ✅ 使用 model_validator 的情况
class User(BaseModel):
password: str
confirm_password: str
@model_validator(mode='after')
def validate_passwords(self):
# 需要比较两个字段
if self.password != self.confirm_password:
raise ValueError('密码不匹配')
return self
执行顺序
用户输入
↓
model_validator(mode='before') # 预处理
↓
field_validator(mode='before') # 每个字段
↓
类型转换
↓
field_validator(mode='after') # 每个字段
↓
model_validator(mode='after') # 整体验证
↓
最终模型
最佳实践
1. 验证器命名规范
# ✅ 推荐
@field_validator('email')
def validate_email(cls, v: str) -> str:
return v
@model_validator(mode='after')
def validate_passwords_match(self):
return self
# ❌ 不推荐
@field_validator('email')
def check(cls, v):
return v
2. 类型注解
# ✅ 推荐:明确标注输入输出类型
@field_validator('age')
def validate_age(cls, v: int) -> int:
return v
# ❌ 不推荐:缺少类型注解
@field_validator('age')
def validate_age(cls, v):
return v
3. 错误消息
# ✅ 推荐:清晰的错误消息
@field_validator('age')
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('年龄不能为负数')
if v > 150:
raise ValueError('年龄不能超过150岁')
return v
# ❌ 不推荐:模糊的错误消息
@field_validator('age')
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('Invalid age')
return v
4. 数据清理
# ✅ 推荐:在验证器中清理数据
@field_validator('email')
def normalize_email(cls, v: str) -> str:
return v.strip().lower()
@field_validator('name')
def clean_name(cls, v: str) -> str:
return v.strip()
5. 性能考虑
# ✅ 推荐:简单的验证用 field_validator
@field_validator('email')
def validate_email_format(cls, v: str) -> str:
if '@' not in v:
raise ValueError('Invalid email')
return v
# ❌ 不推荐:简单验证误用 model_validator
@model_validator(mode='after')
def validate_email_format(self):
if '@' not in self.email:
raise ValueError('Invalid email')
return self
6. 验证顺序
class User(BaseModel):
username: str
email: str
age: int
# 先验证单个字段
@field_validator('username')
def validate_username(cls, v: str) -> str:
if len(v) < 3:
raise ValueError('用户名至少3个字符')
return v
@field_validator('email')
def validate_email(cls, v: str) -> str:
if '@' not in v:
raise ValueError('邮箱格式错误')
return v
# 后验证字段关系
@model_validator(mode='after')
def validate_user(self):
# 此时所有字段都已验证过
return self
参考资源