3. 请求体校验与序列化
任务说明
构建一个FastAPI接口,接收包含用户注册信息的JSON请求体。要求使用Pydantic模型定义请求体结构,实现字段类型、取值范围和格式的自动校验;当请求数据非法时,返回包含具体错误字段和原因的清晰错误信息;当请求合法时,返回序列化后的用户数据。
核心实现
# main.py
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, EmailStr, field_validator
from datetime import datetime
from typing import Optional
app = FastAPI(title="User Registration API")
# ---------- Pydantic 请求体模型 ----------
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=20, description="用户名,3-20个字符")
email: EmailStr = Field(..., description="合法邮箱地址")
age: int = Field(..., ge=18, le=120, description="年龄,18-120岁")
password: str = Field(..., min_length=8, max_length=64, description="密码,8-64位")
nickname: Optional[str] = Field(None, max_length=30, description="昵称,可选")
@field_validator("username")
@classmethod
def username_no_spaces(cls, v: str) -> str:
"""用户名不能包含空格"""
if " " in v:
raise ValueError("用户名不能包含空格")
return v
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
"""密码必须同时包含字母和数字"""
if not any(c.isalpha() for c in v) or not any(c.isdigit() for c in v):
raise ValueError("密码必须同时包含字母和数字")
return v
# ---------- 响应模型(序列化输出) ----------
class UserOut(BaseModel):
id: int
username: str
email: EmailStr
age: int
nickname: Optional[str]
created_at: datetime
# ---------- 模拟存储 ----------
fake_db = {}
user_id_counter = 1
# ---------- 路由 ----------
@app.post("/users/",
response_model=UserOut,
status_code=status.HTTP_201_CREATED,
summary="创建新用户")
async def create_user(user: UserCreate):
"""
接收用户注册数据,自动校验后创建用户。
校验失败时FastAPI自动返回422错误,包含字段级错误详情。
"""
global user_id_counter
# 业务校验:用户名唯一性
if any(u["username"] == user.username for u in fake_db.values()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"用户名 '{user.username}' 已被注册"
)
# 创建用户记录
user_id = user_id_counter
user_id_counter += 1
user_record = {
"id": user_id,
"username": user.username,
"email": user.email,
"age": user.age,
"nickname": user.nickname,
"created_at": datetime.utcnow(),
"password_hash": f"hashed_{user.password}" # 实际项目中应使用安全哈希
}
fake_db[user_id] = user_record
# 返回序列化后的用户数据(不包含密码)
return UserOut(**user_record)
运行与验证
# 安装依赖
pip install fastapi uvicorn pydantic[email]
# 启动服务
uvicorn main:app --reload --port 8000
启动后访问 http://127.0.0.1:8000/docs 可查看自动生成的交互式API文档。
验证用例与预期结果
| 场景 | 请求体 | 预期响应 |
|---|---|---|
| 合法请求 | {"username":"alice","email":"alice@example.com","age":25,"password":"abc12345","nickname":"小爱"} | 201,返回含id、created_at的用户对象 |
| 字段缺失 | {"username":"bob"} | 422,错误信息指明缺少email、age、password字段 |
| 类型错误 | {"username":"alice","email":"alice@example.com","age":"25","password":"abc12345"} | 422,错误信息指明age应为整数 |
| 约束违反 | {"username":"alice","email":"alice@example.com","age":15,"password":"abc12345"} | 422,错误信息指明age小于最小值18 |
| 格式错误 | {"username":"alice","email":"not-an-email","age":25,"password":"abc12345"} | 422,错误信息指明email格式非法 |
| 自定义校验失败 | {"username":"alice","email":"alice@example.com","age":25,"password":"abcdefgh"} | 422,错误信息指明密码缺少数字 |
| 业务冲突 | 重复提交合法alice数据 | 400,detail为"用户名 'alice' 已被注册" |
判定标准:上述非法请求均返回HTTP 422状态码,且响应体detail数组中包含loc(出错字段路径)、msg(错误原因)、type(错误类型)三个关键信息;合法请求返回201及正确的序列化JSON。通过/docs页面或curl命令均可验证。
# curl 验证示例(非法请求)
curl -X POST http://127.0.0.1:8000/users/ \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"bad-email","age":15,"password":"short"}'
# 预期返回(示例输出,非实测值)
# {"detail":[{"loc":["body","email"],"msg":"value is not a valid email address: ...","type":"value_error"},{"loc":["body","age"],"msg":"Input should be greater than or equal to 18","type":"greater_than_equal"},{"loc":["body","password"],"msg":"String should have at least 8 characters","type":"string_too_short"}]}
关键说明
- 自动校验:FastAPI通过类型注解将
UserCreate声明为请求体模型,所有字段约束在请求进入路由函数前自动执行,无需手动编写校验代码。 - 错误信息结构:校验失败时返回的
detail数组包含loc(定位到具体字段)、msg(人类可读原因)、type(机器可读错误类型),便于前端精确提示。 - 序列化隔离:响应模型
UserOut不包含password字段,确保敏感数据不会出现在响应中;created_at由服务端生成后自动序列化为ISO格式。 - 自定义校验:
@field_validator装饰器用于实现内置约束无法表达的规则(如密码复杂度、用户名无空格),验证失败时抛出ValueError即可被FastAPI捕获并转为422响应。