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"}]}
请求体校验与序列化 — 数据流与错误处理 客户端 POST /users/ 请求体 (JSON) {"username": "alice", "email": "alice@ex.com", "age": 25, "password": "abc12345"} FastAPI 路由 async def create_user() Pydantic 模型 UserCreate • 字段类型检查 • 约束验证 (ge/le/min) • EmailStr 格式 • @field_validator 自定义 业务逻辑 用户名唯一性检查 fake_db 存储 user_id → user_record 不含密码字段 UserOut 序列化 response_model 422 错误响应 {"detail": [ {"loc": ["body","age"], "msg": "Input should be >= 18", "type": "greater_than_equal"}]} 201 成功响应 UserOut JSON (含id, created_at) HTTP请求 自动校验 校验失败 校验通过 写入 读取 422 + 字段级错误详情 201 + 序列化用户数据 图例 请求/逻辑 Pydantic校验 错误路径 成功路径 数据存储

关键说明

  • 自动校验:FastAPI通过类型注解将UserCreate声明为请求体模型,所有字段约束在请求进入路由函数前自动执行,无需手动编写校验代码。
  • 错误信息结构:校验失败时返回的detail数组包含loc(定位到具体字段)、msg(人类可读原因)、type(机器可读错误类型),便于前端精确提示。
  • 序列化隔离:响应模型UserOut不包含password字段,确保敏感数据不会出现在响应中;created_at由服务端生成后自动序列化为ISO格式。
  • 自定义校验:@field_validator装饰器用于实现内置约束无法表达的规则(如密码复杂度、用户名无空格),验证失败时抛出ValueError即可被FastAPI捕获并转为422响应。