跳到主要内容

FastAPI + Pydantic 数据校验实战:Field 约束与自定义字段验证器

在上一步完成学生信息的添加接口后,你会发现接口虽然能跑通,但数据质量完全不受控:姓名只写一个“张”也能提交,学号可以随便填 ABCD,性别能输入任意字符,手机号、生日更是毫无约束。本文就基于 Pydantic 的校验能力,给 StudentCreate / StudentUpdate 模型补上完整的字段校验。

一、目标:需要约束哪些字段​

先梳理一下现有模型存在的问题与期望规则:

字段问题期望规则
name单个字符也能通过长度 2~50 个字符,且不允许纯空白
student_no可随意输入 ABCD10 位数字,且前四位在 2000~2099 之间
gender可输入任意内容只能是“男”或“女”,默认“男”
birthday无任何约束年龄应在 5~100 岁之间
phone无任何约束11 位手机号,1 开头、第二位 3~9
class_id无任何约束大于等于 1

对应的实现手段分为四类:

  1. Field 自身约束:长度、正则、枚举、数值范围等;
  2. Field Validator 字段验证器:针对单个字段做自定义逻辑校验(如姓名去空白、学号前缀判断);
  3. 模型级验证:跨字段联合校验(本文暂不展开);
  4. 查询参数快速约束:用于 Query 参数校验(本文暂不展开)。

本篇重点落地前两种。

二、代码实现:用 Field 给字段加约束​

打开定义模型的代码文件,找到 Student、StudentCreate、StudentUpdate,先从 Pydantic 导入 Field,然后逐个字段改造。

1. 姓名:长度 + 描述 + 示例​

name: str = Field(
...,
min_length=2,
max_length=50,
title="姓名",
description="学生姓名长度是2到50个字符",
examples=["Andy"],
)

examples 会在接口文档中作为示例数据展示,方便调用方知道该怎么填。

2. 学号:正则限定 10 位数字​

student_no: str = Field(
...,
pattern=r"^\d{10}$",
title="学号",
description="学号,长度是10位数字",
examples=["2026000001"],
)

\d 表示数字,{10} 表示长度,^ 与 $ 分别锁定开头和结尾,保证整串匹配。

3. 性别:枚举限定​

从 typing 导入 Literal,把类型声明为字面量枚举:

gender: Literal["男", "女"] = Field(
default="男",
title="性别",
description="性别只能为男或女",
)

4. 出生日期、手机号、班级 ID​

birthday: date = Field(
...,
title="出生日期",
description="年龄应该在5-100岁之间",
examples=["2000-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",
)

手机号正则含义:1 开头,第二位是 3~9,后面再跟 9 位数字,共 11 位。

三、代码实现:Field Validator 自定义校验​

Field 的通用约束覆盖不了的逻辑,就用 @field_validator 单独写。先导入:

from pydantic import field_validator

1. 姓名不允许为纯空白​

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

装饰器指定校验的字段名,配合 @classmethod 定义类方法,参数 v 是字段当前值,返回值即校验后的值。这里先 strip() 去掉首尾空白,若结果为空则抛出 ValueError。如果还有“只能是中文或英文”之类的需求,可以继续用 if 加正则判断,不满足就抛异常。

2. 学号前四位必须在 2000~2099 之间​

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

取学号前四位转成整数,判断是否落在区间内,否则抛出异常。

完整代码如下:

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

四、启动服务与接口文档验证​

写完代码启动服务时,第一次运行报了 field_validator 未定义 的错误,原因是忘记导入。在 Pydantic 的导入语句中补上 field_validator 后重新启动即可成功。

FastAPI 服务启动成功

启动成功后访问接口文档,进入添加学生信息的接口点击 Try it out,可以看到每个字段都自动填充了我们配置的 examples 示例数据:name 默认是 Andy,gender 默认是“男”,birthday 也带上了示例日期。这说明 Field 的 title、description、examples 已经正确反映到文档中。

接口文档中自动填充的示例数据

五、测试校验效果​

测试一:姓名为纯空白​

把 name 改成若干空格后点击 Execute,接口返回 422 校验失败,错误信息明确指出 name 字段的 ValueError:姓名不能为空白字符。

姓名为空白字符时返回 422 校验失败

小插曲:第一次测试时返回的是 500 服务器错误,提示“小于等于不支持在整型和字符串上比较”。原因是学号校验函数里把 v(字符串)直接拿去和 2000 比较,改为使用转换后的 year 变量判断后就正常了。

测试二:学号前缀不合法​

把学号改成 10 开头的值再次运行,接口同样返回校验失败,错误信息为:学号前四位应在 2000-2099 之间。

学号前四位不在 2000-2099 时校验失败

六、小结​

本篇通过两类手段完成了学生模型的字段校验:

  • Field 约束:用 min_length / max_length 控制姓名长度,用 pattern 正则约束学号与手机号格式,用 Literal 限定性别枚举,用 ge 约束班级 ID 下限,同时借助 title、description、examples 让接口文档更友好;
  • Field Validator:用 @field_validator + @classmethod 实现姓名去空白判空、学号年份区间判断等自定义逻辑,校验不通过时抛出 ValueError,FastAPI 会自动转换为 422 响应并给出具体错误原因。

至此,单字段的复杂校验已经全部生效。后续如果需要跨字段联合校验(例如验证某字段时同时依赖其他字段),可以继续使用模型级验证器来实现。

课程推荐

想系统学习这门课程?

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

查看课程详情