跳到主要内容

Pydantic 跨字段校验实战:用 model_validator 校验生日与学号的业务关联

在上一节中,我们使用 Field 和 field_validator 完成了单个字段的校验。但在真实业务里,很多规则并不是孤立存在的——比如「出生日期」和「学号」必须联合起来判断才合理。本文就带你用 model_validator 实现跨字段校验。

一、需求分析:为什么要跨字段校验​

先明确两个业务规则:

  1. 年龄校验:用今天的年份减去出生年份得到年龄,年龄必须在 5 到 100 岁之间。
  2. 入学年龄校验:学号前四位代表入学年份,用「入学年份 − 出生年份」得到入学年龄,正常应在 5 到 20 岁之间(放宽后仍异常则说明数据有问题)。

这里的关键在于:入学年龄同时依赖 birthday 和 studentno 两个字段。field_validator 一次只能处理一个字段,无法胜任,因此需要引入 model_validator。

model_validator 的 mode 参数支持两个取值:

  • after:其它字段全部校验完成后再执行本校验(联合校验通常选它);
  • before:在字段校验之前执行。

二、代码实现​

1. 导入 model_validator​

在 Pydantic 中导入 model_validator:

from pydantic import model_validator

2. 编写跨字段校验方法​

在模型类中新增一个校验方法,mode 设为 after,方法返回对象自身:

@model_validator(mode="after")
def validate_birthday_and_studentno(self) -> "StudentCreate":
# birthday 允许为空,为空时直接放行
if self.birthday is None:
return self

# 计算当前年龄
today = datetime.date.today()
age_now = today.year - self.birthday.year
if not (5 <= age_now <= 100):
raise ValueError("年龄应该是5-100之间")

# 计算入学年龄:学号前四位为入学年份
age_at_enrollment = int(self.studentno[:4]) - self.birthday.year
if not (5 <= age_at_enrollment <= 10):
raise ValueError("出生日期与学号不符")

return self

要点说明:

  • 由于 birthday 之前被设置为可为 None,方法开头先做空值判断,为空则直接 return self;
  • 用 datetime.date.today() 获取今天日期,用年份相减得到年龄;
  • 学号前四位通过 self.studentno[:4] 截取并转为 int,再减去出生年份得到入学年龄;
  • 校验不通过时抛出 ValueError,校验通过后务必 return self。

代码编写完成后的模型结构如下:

model_validator 跨字段校验代码编写完成

完整代码如下:


class StudentCreate(BaseModel):
name: str = Field(min_length=2, max_length=50, title="姓名", description="学生姓名长度是2-50个字符",examples=["andy", "安迪"])
student_no: str = Field(pattern=r"^\d{10}$", title="学号", description="学号长度是10位数字",examples=["2026010101"])
gender: Literal["男", "女"] = Field(title="性别", default="男", description="性别只能为男或女")
birthday: datetime.date | None = Field(default=None, title="出生日期", description="应该在5-100岁之间", examples=["2020-01-01"])
phone: str = Field(pattern=r"^1[3-9]\d{9}$",title="手机号", description="手机号长度为11位")
class_id: int = Field(ge=1, title="班级ID", description="班级ID大于1")

@field_validator("name")
@classmethod
def clean_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("姓名不能为空白字符")
return v

@field_validator("student_no")
@classmethod
def validate_student_no_year(cls, v: str) -> str:
# 学号前4位应该是2000-2099
year = int(v[:4])
if not (2000 <= year <= 2099):
raise ValueError("学号前4为应该为2000-2099之间")
return v

@model_validator(mode="after")
def validate_birthday_and_student_no(self) -> "StudentCreate":
if self.birthday is None:
return self

today = datetime.date.today()
age_now = today.year - self.birthday.year
if not( 5 <= age_now <= 100):
raise ValueError("年龄应该是5-100之间")

age_at_enrollment = int(self.student_no[:4]) - self.birthday.year

if not ( 5<= age_at_enrollment <= 10):
raise ValueError("出生日期与学号不符")

return self

三、测试验证​

打开接口文档的 Try it out 面板,依次验证三种场景。

场景一:入学年龄过小​

将出生日期设为 2020 年、学号设为 2022,意味着 2 岁就入学,显然不合理。执行后接口返回错误提示「出生日期与学号不符」,说明跨字段规则生效:

测试入学年龄过小返回出生日期与学号不符

场景二:年龄超出范围​

将出生日期改为 1020 年,年龄远超 100 岁上限。执行后返回「年龄应该是5-100之间」,年龄校验同样生效:

测试年龄超出范围返回年龄应为5到100之间

场景三:正常数据​

使用出生日期 2020 年、学号 2026,入学年龄为 6 岁,符合规则。注意此时若学号已存在会提示重复,换一个未使用的学号后即可提交成功:

测试合法数据提交成功

再试一组学号 2025、5 岁上学,同样创建成功,说明边界值也能正常通过:

测试5岁入学边界值创建成功

四、小结​

本文通过「生日 + 学号」的联合校验,演示了 model_validator 的完整用法:

  • field_validator 只能校验单个字段,model_validator 可以同时使用多个字段,适合处理跨字段业务规则;
  • mode="after" 表示在其它字段全部校验完成后再执行联合校验,是联合校验的常用选择;
  • 校验方法中记得处理可空字段,并在校验通过后 return self。

至此,跨字段业务规则校验就实现完毕了,下节课再见。

课程推荐

想系统学习这门课程?

你已阅读完本文。需要更完整的知识体系,可以继续了解「FastAPI 项目实战」。

查看课程详情