Pydantic校验方式详解

2026/7/29 Pydantic数据校验

# Pydantic 四种校验方式详解

Pydantic 是 Python 中最流行的数据校验库,它提供了多种校验方式来满足不同场景的需求。本文将从简单到复杂,逐一演示 Pydantic 的四种核心校验方式,并给出选择建议。

# 1. Field() 声明式约束

最简单的校验方式,直接在 Field() 参数中声明约束条件,不需要任何装饰器,Pydantic 自动校验。

from pydantic import BaseModel, Field

class FieldConstraintDemo(BaseModel):
    username: str = Field(..., min_length=3, max_length=32)
    password: str = Field(..., min_length=6, max_length=128)
    age: int = Field(default=0, ge=0, le=150)
    status: int = Field(default=0, ge=0, le=1)

支持的常用约束参数:

参数 说明 适用类型
min_length / max_length 字符串长度范围 str
ge / le 大于等于 / 小于等于 int, float
gt / lt 大于 / 小于 int, float
pattern 正则匹配 str
default 默认值 任意

使用示例:

# 合法输入
m = FieldConstraintDemo(username="admin", password="123456")
print(f"username={m.username}, age={m.age}, status={m.status}")

# 非法输入 → 自动抛出 ValidationError
try:
    FieldConstraintDemo(username="ab", password="123456")  # username 长度 < 3
except ValidationError as e:
    print(e)

try:
    FieldConstraintDemo(username="admin", password="123456", age=-1)  # age 负数
except ValidationError as e:
    print(e)

# 2. Annotated[AfterValidator] — 类型注解式校验

核心语法:Annotated[类型, AfterValidator(函数)],将校验逻辑封装为可复用的类型别名,字段直接用类型别名即可,无需写任何装饰器。

import re
from typing import Annotated
from datetime import datetime
from pydantic import AfterValidator, BaseModel

# 定义校验函数(纯函数,可复用)
def _datetime_validator(value: str | datetime) -> datetime:
    """日期时间校验函数"""
    if isinstance(value, datetime):
        return value
    return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")

def _mobile_validator(value: str | None) -> str | None:
    """手机号校验函数"""
    if not value:
        return value
    if len(value) != 11 or not value.isdigit():
        raise ValueError("手机号格式不正确")
    if not re.match(r"^1\d{10}$", value):
        raise ValueError("手机号格式不正确")
    return value

def _email_validator(value: str) -> str:
    """邮箱校验函数"""
    if not value:
        raise ValueError("邮箱地址不能为空")
    if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", value):
        raise ValueError("邮箱地址格式不正确")
    return value

# 定义可复用的类型别名
DateTimeStr = Annotated[datetime, AfterValidator(_datetime_validator)]
Telephone = Annotated[str, AfterValidator(_mobile_validator)]
Email = Annotated[str, AfterValidator(_email_validator)]

class AnnotatedDemo(BaseModel):
    """使用类型别名字段,@field_validator 都不用写"""
    created_time: DateTimeStr | None = None
    phone: Telephone | None = None
    email: Email | None = None

使用示例:

# 日期时间
m = AnnotatedDemo(created_time="2024-01-15 08:30:00")
print(f"日期时间: {m.created_time}")

# 手机号
m = AnnotatedDemo(phone="13800138000")
print(f"手机号: {m.phone}")

# 邮箱
m = AnnotatedDemo(email="[email protected]")
print(f"邮箱: {m.email}")

# 非法输入 → ValidationError
try:
    AnnotatedDemo(phone="12345678901")  # 非法手机号
except ValidationError:
    print("非法手机号 → 拦截")

核心优势:一处定义,全局复用。 DateTimeStrTelephoneEmail 这些类型别名可以在任何 Model 中直接使用。

# 3. @field_validator — 单字段校验装饰器

当某个字段有特殊的校验逻辑,无法用 Field() 或类型别名表达时,使用 @field_validator 装饰器。

import re
from pydantic import BaseModel, field_validator, ValidationError

class FieldValidatorDemo(BaseModel):
    username: str
    password: str

    @field_validator("username")
    @classmethod
    def check_username(cls, value: str):
        """校验账号:字母开头,3-32 位"""
        v = value.strip()
        if not v:
            raise ValueError("账号不能为空")
        if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
            raise ValueError("账号需以字母开头,3-32 位")
        return v

    @field_validator("password")
    @classmethod
    def check_password(cls, value: str):
        if len(value) < 6:
            raise ValueError("密码长度不能少于 6 位")
        if len(value) > 128:
            raise ValueError("密码长度不能超过 128 位")
        return value

使用示例:

# 合法输入
m = FieldValidatorDemo(username="TestUser_1", password="pass123")
print(f"合法: username={m.username}")

# 非法输入
try:
    FieldValidatorDemo(username="1abc", password="123456")  # 账号不以字母开头
except ValidationError:
    print("非法账号 → 拦截")

注意事项:

  • 必须配合 @classmethod 使用
  • 校验通过后返回值会替换原值
  • 可以在 @field_validator("field", mode="before") 中做数据预处理

# 4. @model_validator(mode="after") — 多字段交叉校验

当校验逻辑需要访问多个字段时,使用 @model_validator(mode="after"),可以访问 self 上所有字段。

from pydantic import BaseModel, model_validator, ValidationError

class OrderDemo(BaseModel):
    start_time: DateTimeStr | None = None
    end_time: DateTimeStr | None = None
    order_type: str = "package"
    plugin_id: int | None = None
    package_id: int | None = None

    @model_validator(mode="after")
    def check_time_range(self):
        """校验1:结束时间 >= 开始时间"""
        if self.start_time and self.end_time and self.start_time >= self.end_time:
            raise ValueError("结束时间不能早于或等于开始时间")
        return self

    @model_validator(mode="after")
    def check_target(self):
        """校验2:条件必填"""
        if self.order_type == "plugin":
            if not self.plugin_id or self.plugin_id <= 0:
                raise ValueError("插件订单必须指定 plugin_id")
        else:
            if not self.package_id or self.package_id <= 0:
                raise ValueError("套餐订单必须指定 package_id")
        return self

使用示例:

# 时间范围正确
m = OrderDemo(start_time="2024-01-01 00:00:00", end_time="2024-12-31 23:59:59", package_id=1)
print(f"时间范围正确: {m.start_time} ~ {m.end_time}")

# 时间范围错误
try:
    OrderDemo(start_time="2024-12-31 23:59:59", end_time="2024-01-01 00:00:00", package_id=1)
except ValidationError:
    print("结束 < 开始 → 拦截")

# 条件必填:plugin 需要 plugin_id
try:
    OrderDemo(order_type="plugin")
except ValidationError:
    print("plugin 缺 id → 拦截")

注意事项:

  • 验证通过必须 return self
  • 可以定义多个 @model_validator(mode="after") 方法
  • 适合做:时间范围校验、条件必填、字段间依赖关系

# 5. @model_validator(mode="before") — 原始数据清洗

在 Pydantic 解析类型之前处理原始 dict,适合做数据清洗和类型转换。

from pydantic import BaseModel, model_validator, ValidationError

class MenuDemo(BaseModel):
    name: str
    parent_id: int | None = None
    client: str = "pc"
    component_path: str | None = None

    @model_validator(mode="before")
    @classmethod
    def clean_data(cls, values: dict) -> dict:
        """在 Pydantic 解析之前清洗原始数据"""
        if not isinstance(values, dict):
            return values

        # 字符串去空格,空串→None
        for key in ["name", "component_path"]:
            if key in values and isinstance(values[key], str):
                stripped = values[key].strip()
                values[key] = stripped or None

        # parent_id 字符串→int
        if "parent_id" in values and isinstance(values["parent_id"], str):
            try:
                values["parent_id"] = int(values["parent_id"].strip())
            except (ValueError, TypeError):
                pass

        # client 非法值降级
        if "client" in values and isinstance(values["client"], str):
            cv = values["client"].strip()
            values["client"] = cv if cv in ("pc", "app") else "pc"

        # component_path 不能以 / 开头
        if "component_path" in values and isinstance(values["component_path"], str):
            cp = values["component_path"].strip()
            if cp and cp.startswith("/"):
                raise ValueError("组件路径不能以 / 开头")
            values["component_path"] = cp

        return values

使用示例:

# 去空格
s = MenuDemo(name="  hello  ")
print(f"去空格: '{s.name}'")  # 'hello'

# parent_id 转 int
s = MenuDemo(name="test", parent_id="5")
print(f"parent_id: {s.parent_id} (类型: {type(s.parent_id).__name__})")  # 5, int

# client 降级
s = MenuDemo(name="test", client="ios")
print(f"client 降级: '{s.client}'")  # 'pc'

# component_path 以 / 开头 → 抛错
try:
    MenuDemo(name="test", component_path="/views/test")
except ValidationError:
    print("组件路径以 / 开头 → 拦截")

注意事项:

  • 接收 cls + dict,必须 return dict
  • 适合做:字符串去空格、类型转换、空串→None、值降级
  • 数据清洗在类型解析之前执行,清洗后的数据再进入后续校验流程

# 6. 纯函数校验器

最基础的校验方式:一个普通函数,抛异常表示失败。可被 AfterValidator / @field_validator / 手动调用复用。

# 纯函数可以直接手动调用
dt = _datetime_validator("2024-01-15 08:30:00")
print(f"datetime_validator: {dt}")

try:
    _datetime_validator("abc")
except ValueError:
    print("非法日期 → 拦截")

# 也可以通过 AfterValidator 在 Pydantic 中使用
DateTimeStr = Annotated[datetime, AfterValidator(_datetime_validator)]

纯函数是所有校验方式的基础,AfterValidator@field_validator 本质上都是对纯函数的封装。

# 四种方式对比总结

方式 使用形式 适用场景 代码量
Field() 声明式约束 非装饰器 长度/范围/正则等简单约束 最少
Annotated[AfterValidator] 非装饰器(类型注解) 全局通用的格式(手机号/邮箱/时间) 中(一次定义全局复用)
@field_validator 装饰器 单字段特殊格式
@model_validator(mode='after') 装饰器 多字段交叉/条件必填
@model_validator(mode='before') 装饰器 原始数据清洗/类型转换
纯函数 非装饰器 可被以上所有方式复用 最少

# 选择建议

  1. 简单长度/范围Field(min_length=..., ge=...)
  2. 全局通用格式(手机号)Annotated[类型, AfterValidator(函数)]
  3. 单个字段特殊规则@field_validator
  4. 多字段交叉/条件必填@model_validator(mode='after')
  5. 前端数据清洗@model_validator(mode='before')
  6. 以上都可再组合纯函数def validator(x): ...
Last Updated: 2026/7/28 16:36:17