Pydantic 验证器完全指南

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

参考资源

posted @ 2026-03-03 19:16  shyNi  阅读(184)  评论(0)    收藏  举报