Skip to content

第 6 章 · 响应模型与状态码

本章目标:掌握 response_model 的输出过滤能力及其安全意义,学会输入/输出模型分离,正确设置状态码与文档元信息。

6.1 为什么需要响应模型

路径函数的返回值默认原样序列化。如果数据库对象里带着 password_hashinternal_notes 这类字段,裸返回就是安全事故。response_model 让你声明客户端能看到什么

python
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    # 输入模型:包含明文密码(仅用于注册接收)
    username: str
    email: EmailStr
    password: str


class UserOut(BaseModel):
    # 输出模型:绝不包含密码
    username: str
    email: EmailStr


@app.post("/user/", response_model=UserOut)
def create_user(user: UserIn):
    # 假设已存库。这里直接把含密码的 user 返回,
    # 但 response_model=UserOut 会过滤掉 password 字段
    return user

即使函数返回了完整对象,客户端也只会收到 usernameemail过滤是框架强制的,不依赖开发者的自觉——这就是响应模型的安全意义。

response_model装饰器的参数,不是函数参数,这是初学者常犯的书写错误。

6.2 返回类型注解 vs response_model

现代 FastAPI 支持两种声明方式:

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


# 方式一:返回类型注解(推荐,编辑器/静态检查都认识)
@app.get("/items/a", response_model=Item)
def get_a() -> Item:
    return Item(name="pen", price=3.0)


# 方式二:仅用返回类型注解,FastAPI 同样会据此生成文档并过滤
@app.get("/items/b")
def get_b() -> Item:
    return Item(name="book", price=42.0)

两者同时存在时 response_model 优先。这个设计专门服务于一类场景:函数实际返回的是数据库 ORM 对象或 dict,与声明的模型类型不同——返回类型注解写给 mypy 看,response_model 写给 FastAPI 做"文档+校验+过滤":

python
from typing import Any

@app.get("/db-items/{item_id}", response_model=Item)
def get_db_item(item_id: int) -> Any:
    # 返回 SQLAlchemy ORM 对象,类型上与 Item 不符,
    # 用 Any 注解骗过静态检查,FastAPI 仍按 Item 序列化过滤
    return fake_db.get(item_id)

6.3 输入/输出模型分离的标准模式

注册场景的完整三模型写法(官方教程模式):

python
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    email: EmailStr
    password: str


class UserOut(BaseModel):
    username: str
    email: EmailStr


class UserDB(UserIn):          # 内部模型:继承输入模型,再加哈希字段
    password_hash: str


def fake_hash(pwd: str) -> str:
    return "hashed-" + pwd


@app.post("/users/", response_model=UserOut)
def register(user: UserIn):
    db_user = UserDB(
        **user.model_dump(),
        password_hash=fake_hash(user.password),
    )
    # 返回的 db_user 含 password 和 password_hash,
    # 但 response_model=UserOut 只放行 username/email
    return db_user

别把 UserIn 直接当响应模型

如果 response_model=UserIn,明文密码会回显给客户端。永远为"输入"和"输出"定义不同的模型,哪怕字段暂时一样——它们会独立演进。

6.4 精细控制输出字段

response_model 还支持包含/排除控制:

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float
    description: str | None = None
    tax: float | None = None


items_db = {
    "foo": {"name": "Foo", "price": 50.2, "description": "特价商品", "tax": 5.2},
}


@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: str):
    return items_db[item_id]


# 只输出 description 和 tax 两个字段
@app.get("/items-attrs/{item_id}",
         response_model=Item,
         response_model_include={"description", "tax"})
def read_item_attrs(item_id: str):
    return items_db[item_id]


# 排除 tax 字段
@app.get("/items-public/{item_id}",
         response_model=Item,
         response_model_exclude={"tax"})
def read_item_public(item_id: str):
    return items_db[item_id]

include/exclude 的适用边界

这两个参数适合零星微调;字段差异大时仍应定义独立模型——模型是显式契约,include/exclude 是隐式补丁。另外 response_model_exclude_none=True 可自动省略值为 None 的字段,对移动端省流量很有用。

6.5 状态码与文档元信息

装饰器参数 status_code 设置成功响应的状态码;tags/summary/description 丰富文档:

python
from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()


class Post(BaseModel):
    title: str
    content: str


@app.post(
    "/posts/",
    status_code=status.HTTP_201_CREATED,   # 创建成功用 201,而不是默认 200
    tags=["文章"],
    summary="发布新文章",
)
def create_post(post: Post):
    return post


@app.delete("/posts/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(post_id: int):
    # 204 表示成功但无响应体,不要 return 任何内容
    return None

推荐用 status 模块的具名常量而不是魔法数字:HTTP_201_CREATEDHTTP_204_NO_CONTENTHTTP_404_NOT_FOUND 等,代码即文档。

::: note 状态码速记 2xx 成功(200 默认 / 201 已创建 / 204 无内容);4xx 客户端错误(422 校验失败 / 401 未认证 / 403 无权限 / 404 不存在);5xx 服务端错误。REST 设计中"创建返回 201"是最常被忽略的一条。 :::

6.6 本章小结

  • response_model 强制过滤输出字段,是防敏感信息泄漏的框架级保障;
  • 它是装饰器参数,与返回类型注解同时存在时优先级更高,专门用于返回 ORM/dict 的场景;
  • 输入模型(UserIn)与输出模型(UserOut)必须分离,内部模型可继承输入模型扩展;
  • response_model_include/exclude/exclude_none 做零星微调,大差异仍用独立模型;
  • status_code=status.HTTP_201_CREATED 等具名常量让状态码语义明确,创建资源用 201、删除成功用 204。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. response_model 应该写在哪里?

2. 函数返回了包含 password 字段的完整对象,但装饰器声明 response_model=UserOut(不含 password),客户端会收到?

3. 同时写返回类型注解 -> Item 和装饰器参数 response_model=OtherModel,FastAPI 用哪个?

4. 创建资源成功的 RESTful 惯例状态码是?

🛠️ 动手实践

  1. 为「员工 Employee」实现三模型分离:EmployeeIn(含 salary)、EmployeeOut(不含 salary)、EmployeeDB(含 hired_at 时间戳),创建接口返回过滤后的对象。
  2. response_model_exclude_none=True 改造一个可选字段较多的接口,观察 None 字段从 JSON 中消失的效果。
  3. 给你的 CRUD 接口补齐语义状态码:创建 201、删除 204、并给所有接口加上 tags 分组,在 /docs 里查看效果。

响应已经可控,下一章处理第 7 章表单与文件上传。