请求参数验证(Validation) —— 这是 FastAPI 最强大的特性之一。
FastAPI 基于 Pydantic,提供了多种方式对输入数据进行校验。下面我用 同一个业务案例,分别用 4 种典型验证方式 实现,并对比它们的适用场景、优缺点和特点。
🎯 业务案例:用户注册 API
要求:
username:字符串,3~20 字符,仅允许字母、数字、下划线password:字符串,至少 8 位,包含字母和数字age:整数,13~120 岁email:合法邮箱格式(可选)
我们将用以下 4 种方式实现验证:
- 基础类型提示 + 默认值
- 使用
Query,Path,Body添加简单约束 - 使用 Pydantic
Field+ 正则/范围校验 - 自定义校验器(
@validator或@field_validator)
✅ 方式 1:基础类型提示(最简,但能力有限)
from fastapi import FastAPI
from typing import Optional
app = FastAPI()
@app.post("/register1/")
def register1(
username: str,
password: str,
age: int,
email: Optional[str] = None
):
return {"msg": "Registered!"}🔍 特点:
- ✅ 自动校验基本类型(
str,int) - ❌ 无法校验长度、格式、范围
- ❌ 密码可以是
"1",用户名可以是"a" - ❌ 邮箱可以是
"not-an-email"
💡 适合快速原型,不适合生产环境。
✅ 方式 2:使用 FastAPI 的 Query / Body 添加简单约束
from fastapi import FastAPI, Body
from typing import Optional
app = FastAPI()
@app.post("/register2/")
def register2(
username: str = Body(..., min_length=3, max_length=20),
password: str = Body(..., min_length=8),
age: int = Body(..., ge=13, le=120), # ge=≥, le=≤
email: Optional[str] = Body(None)
):
return {"msg": "Registered!"}🔍 特点:
- ✅ 支持长度、数值范围校验
- ✅ 错误时自动返回 422 + 详细信息
- ❌ 无法校验复杂规则(如“密码必须含字母和数字”)
- ❌ 正则表达式不支持(
Body不支持regex) - ❌ 参数一多,函数签名变得冗长
💡 适合简单字段级约束,但逻辑复杂时不够用。
✅ 方式 3:使用 Pydantic Field(推荐!结构清晰)
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
import re
class UserRegister(BaseModel):
username: str = Field(..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$")
password: str = Field(..., min_length=8)
age: int = Field(..., ge=13, le=120)
email: Optional[str] = Field(None)
app = FastAPI()
@app.post("/register3/")
def register3(user: UserRegister):
return {"msg": "Registered!", "user": user}⚠️ 注意:pattern在 Pydantic v2 中替代了旧版的regex。
🔍 特点:
- ✅ 支持正则表达式(
pattern) - ✅ 所有校验集中在一个模型中,结构清晰
- ✅ 自动生成 OpenAPI 文档中的格式说明
- ❌ 仍无法实现“密码必须同时含字母和数字”这类跨字符逻辑
💡 这是大多数场景的最佳选择!
✅ 方式 4:自定义校验器(处理复杂逻辑)
使用 @field_validator(Pydantic v2 推荐)实现高级校验:
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import re
class UserRegister(BaseModel):
username: str = Field(..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$")
password: str = Field(..., min_length=8)
age: int = Field(..., ge=13, le=120)
email: Optional[str] = Field(None)
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
if not re.search(r"[A-Za-z]", v):
raise ValueError("Password must contain at least one letter")
if not re.search(r"\d", v):
raise ValueError("Password must contain at least one digit")
return v
@field_validator("email")
@classmethod
def validate_email(cls, v: str | None) -> str | None:
if v is not None:
if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", v):
raise ValueError("Invalid email format")
return v
app = FastAPI()
@app.post("/register4/")
def register4(user: UserRegister):
return {"msg": "Registered!", "user": user}🔍 特点:
- ✅ 可实现任意复杂校验逻辑
- ✅ 校验失败时返回友好错误信息
- ✅ 保持模型为中心的设计
- ⚠️ 需要写额外代码,但可测试、可复用
💡 适合安全敏感场景(如密码、身份证、手机号校验)。
📊 四种方式对比总结
| 特性 | 方式1:基础类型 | 方式2:Body/Query | 方式3:Field | 方式4:自定义校验器 |
|---|---|---|---|---|
| 类型校验 | ✅ | ✅ | ✅ | ✅ |
| 长度/范围 | ❌ | ✅ | ✅ | ✅ |
| 正则表达式 | ❌ | ❌ | ✅(pattern) | ✅(任意逻辑) |
| 复杂业务规则 | ❌ | ❌ | ❌ | ✅ |
| 代码可读性 | 高 | 低(参数臃肿) | 高(模型集中) | 中(需理解装饰器) |
| OpenAPI 文档支持 | 基础 | 部分 | 完整(含 pattern) | 校验逻辑不显示 |
| 适用场景 | 快速原型 | 简单 API | 推荐:大多数项目 | 密码、身份证、金融等强校验 |
✅ 最佳实践建议
- 优先使用
BaseModel + Field(方式3)—— 结构清晰,功能强大。 - 复杂规则用
@field_validator(方式4)—— 不要试图用正则解决所有问题。 - 避免在路由函数中写校验逻辑—— 违反关注点分离。
- 利用 FastAPI 自动生成的
/docs测试校验效果!
🧪 测试你的 API
访问 http://localhost:8000/docs,尝试以下无效输入:
username: "ab"→ 太短password: "12345678"→ 缺少字母age: 10→ 小于 13email: "bad-email"→ 格式错误
你会看到 FastAPI 返回清晰的 422 错误,例如:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "password"],
"msg": "Password must contain at least one letter",
"input": "12345678"
}
]
}