第 6 章 · 响应模型与状态码
本章目标:掌握 response_model 的输出过滤能力及其安全意义,学会输入/输出模型分离,正确设置状态码与文档元信息。
6.1 为什么需要响应模型
路径函数的返回值默认原样序列化。如果数据库对象里带着 password_hash、internal_notes 这类字段,裸返回就是安全事故。response_model 让你声明客户端能看到什么:
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即使函数返回了完整对象,客户端也只会收到 username 和 email。过滤是框架强制的,不依赖开发者的自觉——这就是响应模型的安全意义。
response_model 是装饰器的参数,不是函数参数,这是初学者常犯的书写错误。
6.2 返回类型注解 vs response_model
现代 FastAPI 支持两种声明方式:
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 做"文档+校验+过滤":
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 输入/输出模型分离的标准模式
注册场景的完整三模型写法(官方教程模式):
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 还支持包含/排除控制:
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 丰富文档:
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_CREATED、HTTP_204_NO_CONTENT、HTTP_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 惯例状态码是?
🛠️ 动手实践
- 为「员工 Employee」实现三模型分离:EmployeeIn(含 salary)、EmployeeOut(不含 salary)、EmployeeDB(含 hired_at 时间戳),创建接口返回过滤后的对象。
- 用
response_model_exclude_none=True改造一个可选字段较多的接口,观察 None 字段从 JSON 中消失的效果。 - 给你的 CRUD 接口补齐语义状态码:创建 201、删除 204、并给所有接口加上 tags 分组,在 /docs 里查看效果。
响应已经可控,下一章处理第 7 章表单与文件上传。