4. 响应模型与数据过滤

任务说明

本案例要求为FastAPI接口定义明确的响应模型,实现两个目标:一是隐藏敏感字段(如密码、内部ID),二是按需控制输出字段。我们将通过一个用户信息查询接口演示Pydantic响应模型的核心用法。

核心实现

# main.py
from typing import Optional
from fastapi import FastAPI, Query
from pydantic import BaseModel, EmailStr, Field

app = FastAPI()

# ---------- 内部数据模型(含敏感字段) ----------
class UserInDB(BaseModel):
    user_id: int
    username: str
    email: EmailStr
    hashed_password: str = Field(..., description="内部使用,绝不返回")
    is_active: bool = True
    secret_note: Optional[str] = None  # 内部备注,不对外暴露

# ---------- 对外响应模型(安全字段) ----------
class UserPublic(BaseModel):
    username: str
    email: EmailStr
    is_active: bool

# ---------- 带字段控制的响应模型 ----------
class UserProfile(UserPublic):
    user_id: int  # 仅当请求方需要时通过参数控制返回

# ---------- 模拟数据库 ----------
fake_db = {
    1: UserInDB(
        user_id=1,
        username="alice",
        email="alice@example.com",
        hashed_password="fakehash123",
        secret_note="内部备注A",
    ),
    2: UserInDB(
        user_id=2,
        username="bob",
        email="bob@example.com",
        hashed_password="fakehash456",
        is_active=False,
    ),
}

# ---------- 接口1:自动过滤敏感字段 ----------
@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
    """返回用户公开信息,自动剔除 hashed_password 和 secret_note"""
    if user_id not in fake_db:
        return {"detail": "用户不存在"}  # 实际应使用 HTTPException
    return fake_db[user_id]

# ---------- 接口2:通过查询参数控制输出字段 ----------
@app.get("/users/{user_id}/profile", response_model=UserProfile)
def get_user_profile(
    user_id: int,
    include_id: bool = Query(False, description="是否返回 user_id 字段"),
):
    """根据 include_id 参数决定是否返回 user_id"""
    if user_id not in fake_db:
        return {"detail": "用户不存在"}
    user = fake_db[user_id]
    # 构造返回数据,仅包含 UserProfile 中定义的字段
    result = {
        "username": user.username,
        "email": user.email,
        "is_active": user.is_active,
    }
    if include_id:
        result["user_id"] = user.user_id
    return result

关键机制说明

上述代码通过三层模型实现字段控制:UserInDB代表数据库完整记录,包含hashed_password和secret_note等敏感字段;UserPublic作为对外响应模型,仅声明公开字段,FastAPI会自动将UserInDB实例转换为UserPublic结构,未声明的字段被丢弃;UserProfile继承自UserPublic并增加user_id,配合查询参数实现可选字段输出。接口2中手动构造字典是为了演示字段级控制逻辑——当include_id=False时,返回数据中不包含user_id键,FastAPI会按UserProfile模型校验并过滤。

运行与验证

# 安装依赖
pip install fastapi uvicorn pydantic[email]

# 启动服务
uvicorn main:app --reload --port 8000

启动后访问以下URL验证:

  • 接口1:GET http://127.0.0.1:8000/users/1 — 预期返回JSON仅含username、email、is_active三个字段,无hashed_password和secret_note。
  • 接口2(不带参数):GET http://127.0.0.1:8000/users/1/profile — 预期返回不含user_id。
  • 接口2(带参数):GET http://127.0.0.1:8000/users/1/profile?include_id=true — 预期返回包含user_id字段。

判定标准:响应中绝不出现hashed_password或secret_note;接口2的返回字段随include_id参数正确变化;访问不存在的用户(如/users/999)时返回{"detail":"用户不存在"}。

响应模型数据过滤流程 UserInDB(数据库记录) user_id: int username: str email: EmailStr hashed_password: str secret_note: str UserPublic username: str email: EmailStr is_active: bool UserProfile username: str email: EmailStr is_active: bool user_id: int(可选) GET /users/{id} response_model=UserPublic GET /users/{id}/profile response_model=UserProfile ?include_id=true/false 自动过滤 手动构造 预期输出示例 GET /users/1 → {"username":"alice","email":"alice@example.com","is_active":true} GET /users/1/profile?include_id=true → {"username":"alice","email":"alice@example.com","is_active":true,"user_id":1}