示例说明
真实用户的输入永远比想象脏:邮箱带空格混大小写甚至 mailto 前缀、年龄是字符串、邮编混入字母。mode=before 的校验器在类型转换前介入——修剪归一做完再交给类型系统,标准注解的区间约束照常生效;model_validator 则处理跨字段一致性,单字段无法表达的规则在这里收口。
技术要点
- before 模式先归一化再类型转换
- Annotated 注解集中字段约束
- 模型级校验处理跨字段规则
代码解读
normalize_email 修剪、转小写并剥掉 mailto 前缀;digits_only 要求邮编恰好五位纯数字否则抛值错误;passwords_match 在模型构造后比对两次输入。示例输入的年龄是字符串却被正确转为 int,邮箱归一为小写无前缀形态。
使用建议
归一化规则越靠近入口越好,别让脏数据流进业务层。校验失败信息要可指导用户修正。before 校验器里别做网络请求等重操作。
代码示例
from typing import Annotated
from pydantic import BaseModel, Field, field_validator, model_validator
class SignupForm(BaseModel):
email: Annotated[str, Field(min_length=5, max_length=120)]
age: Annotated[int, Field(ge=18, le=120)]
zipcode: str
password: str
password_confirm: str
@field_validator('email', mode='before')
@classmethod
def normalize_email(cls, raw: object) -> object:
if isinstance(raw, str):
candidate = raw.strip().lower()
if candidate.startswith('mailto:'):
candidate = candidate.removeprefix('mailto:')
return candidate
return raw
@field_validator('zipcode', mode='before')
@classmethod
def digits_only(cls, raw: object) -> object:
if isinstance(raw, str) and raw.isdigit() and len(raw) == 5:
return raw
raise ValueError('zipcode must be exactly 5 digits')
@model_validator(mode='after')
def passwords_match(self) -> 'SignupForm':
if self.password != self.password_confirm:
raise ValueError('passwords do not match')
return self
form = SignupForm(
email=' MAILTO:[email protected] ',
age='30',
zipcode='02138',
password='hunter2xx',
password_confirm='hunter2xx',
)
print('normalized email:', form.email, 'age type:', type(form.age).__name__)