Pydantic校验方式详解
zhuib 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("非法手机号 → 拦截")
核心优势:一处定义,全局复用。 DateTimeStr、Telephone、Email 这些类型别名可以在任何 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') | 装饰器 | 原始数据清洗/类型转换 | 中 |
| 纯函数 | 非装饰器 | 可被以上所有方式复用 | 最少 |
# 选择建议
- 简单长度/范围 →
Field(min_length=..., ge=...) - 全局通用格式(手机号) →
Annotated[类型, AfterValidator(函数)] - 单个字段特殊规则 →
@field_validator - 多字段交叉/条件必填 →
@model_validator(mode='after') - 前端数据清洗 →
@model_validator(mode='before') - 以上都可再组合纯函数 →
def validator(x): ...