写 FastAPI 接口时,很多人会把两个东西混在一起:
python
response_class=HTMLResponse
和:
python
response_model=UserResponse
它们都跟响应有关,但管的完全不是同一件事。
response_class 管的是 HTTP 响应类型。
response_model 管的是 JSON 数据结构。
前者更像是在说,这个响应是 HTML、文件、纯文本,还是 JSON。
后者更像是在说,这个接口最终允许暴露哪些字段。
这篇我们就把 FastAPI 响应系统讲清楚。
本篇准备
这一篇继续使用基础依赖:
bash
pip install fastapi uvicorn
如果要运行 FileResponse 示例,需要提前准备一个文件,例如 files/1.jpeg。否则代码本身没问题,但访问接口时会因为文件不存在而报错。
1. 默认 JSON 响应
最常见的写法是直接返回字典:
python
@app.get("/")
async def root():
return {"message": "Hello World"}
FastAPI 会把它变成 JSON 响应。
返回列表也一样:
python
@app.get("/books")
async def get_books():
return [
{"id": 1, "name": "Python"},
{"id": 2, "name": "FastAPI"}
]
大部分 API 接口都属于这种情况。
你返回 Python 对象,FastAPI 负责序列化。
#mermaid-svg-GcL3O4CYnog4NOBD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GcL3O4CYnog4NOBD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GcL3O4CYnog4NOBD .error-icon{fill:#552222;}#mermaid-svg-GcL3O4CYnog4NOBD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GcL3O4CYnog4NOBD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GcL3O4CYnog4NOBD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GcL3O4CYnog4NOBD .marker.cross{stroke:#333333;}#mermaid-svg-GcL3O4CYnog4NOBD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GcL3O4CYnog4NOBD p{margin:0;}#mermaid-svg-GcL3O4CYnog4NOBD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GcL3O4CYnog4NOBD .cluster-label text{fill:#333;}#mermaid-svg-GcL3O4CYnog4NOBD .cluster-label span{color:#333;}#mermaid-svg-GcL3O4CYnog4NOBD .cluster-label span p{background-color:transparent;}#mermaid-svg-GcL3O4CYnog4NOBD .label text,#mermaid-svg-GcL3O4CYnog4NOBD span{fill:#333;color:#333;}#mermaid-svg-GcL3O4CYnog4NOBD .node rect,#mermaid-svg-GcL3O4CYnog4NOBD .node circle,#mermaid-svg-GcL3O4CYnog4NOBD .node ellipse,#mermaid-svg-GcL3O4CYnog4NOBD .node polygon,#mermaid-svg-GcL3O4CYnog4NOBD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GcL3O4CYnog4NOBD .rough-node .label text,#mermaid-svg-GcL3O4CYnog4NOBD .node .label text,#mermaid-svg-GcL3O4CYnog4NOBD .image-shape .label,#mermaid-svg-GcL3O4CYnog4NOBD .icon-shape .label{text-anchor:middle;}#mermaid-svg-GcL3O4CYnog4NOBD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GcL3O4CYnog4NOBD .rough-node .label,#mermaid-svg-GcL3O4CYnog4NOBD .node .label,#mermaid-svg-GcL3O4CYnog4NOBD .image-shape .label,#mermaid-svg-GcL3O4CYnog4NOBD .icon-shape .label{text-align:center;}#mermaid-svg-GcL3O4CYnog4NOBD .node.clickable{cursor:pointer;}#mermaid-svg-GcL3O4CYnog4NOBD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GcL3O4CYnog4NOBD .arrowheadPath{fill:#333333;}#mermaid-svg-GcL3O4CYnog4NOBD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GcL3O4CYnog4NOBD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GcL3O4CYnog4NOBD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GcL3O4CYnog4NOBD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GcL3O4CYnog4NOBD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GcL3O4CYnog4NOBD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GcL3O4CYnog4NOBD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GcL3O4CYnog4NOBD .cluster text{fill:#333;}#mermaid-svg-GcL3O4CYnog4NOBD .cluster span{color:#333;}#mermaid-svg-GcL3O4CYnog4NOBD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GcL3O4CYnog4NOBD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GcL3O4CYnog4NOBD rect.text{fill:none;stroke-width:0;}#mermaid-svg-GcL3O4CYnog4NOBD .icon-shape,#mermaid-svg-GcL3O4CYnog4NOBD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GcL3O4CYnog4NOBD .icon-shape p,#mermaid-svg-GcL3O4CYnog4NOBD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GcL3O4CYnog4NOBD .icon-shape .label rect,#mermaid-svg-GcL3O4CYnog4NOBD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GcL3O4CYnog4NOBD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GcL3O4CYnog4NOBD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GcL3O4CYnog4NOBD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} dict/list/Pydantic model
FastAPI 编码
JSONResponse
客户端
2. response_class,控制响应类型
有时候接口返回的不是 JSON。
比如返回 HTML:
python
from fastapi.responses import HTMLResponse
@app.get("/html", response_class=HTMLResponse)
async def get_html():
return "<h1>你好 FastAPI</h1>"
这里 response_class=HTMLResponse 告诉 FastAPI:
这个接口返回的是 HTML,不要把字符串当成普通 JSON 文本处理。
再比如返回文件:
python
from fastapi.responses import FileResponse
@app.get("/image")
async def get_image():
return FileResponse("./files/1.jpeg")
FileResponse 适合返回图片、PDF、Excel、音视频等文件。
注意,课程示例里用了相对路径 ./files/1.jpeg。学习没问题,但真实项目里更建议用基于当前文件的绝对路径,避免启动目录变化导致找不到文件。
python
from pathlib import Path
from fastapi.responses import FileResponse
BASE_DIR = Path(__file__).resolve().parent
@app.get("/image")
async def get_image():
return FileResponse(BASE_DIR / "files" / "1.jpeg")
这不是语法洁癖,是线上项目里非常常见的路径问题。
3. response_model,控制输出结构
response_model 解决的是另一个问题。
比如数据库里的用户对象可能长这样:
python
{
"id": 1,
"username": "tom",
"password": "$2b$12$xxxx",
"phone": "13800000000"
}
如果你直接返回这个对象,密码哈希、手机号等字段可能被暴露。
这时就应该定义响应模型:
python
from pydantic import BaseModel, ConfigDict
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
@app.get("/user/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
return {
"id": user_id,
"username": "tom",
"password": "hidden"
}
最终响应只会包含:
json
{
"id": 1,
"username": "tom"
}
这就是 response_model 的价值。
它不只是文档说明,而是接口输出契约。
4. 两者的区别
用一张表记最清楚:
| 对比项 | response_class |
response_model |
|---|---|---|
| 控制内容 | HTTP 响应类型 | JSON 数据结构 |
| 解决问题 | 客户端如何理解响应 | 接口暴露哪些字段 |
| 常见值 | HTMLResponse、FileResponse、PlainTextResponse |
Pydantic 模型 |
| 典型场景 | 返回 HTML、文件、纯文本 | 隐藏敏感字段、统一输出格式 |
| 是否影响 OpenAPI | 会影响媒体类型 | 会影响 schema |
选择时可以这样判断:
#mermaid-svg-DCaNG4sx3y4ZCXo3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .error-icon{fill:#552222;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .marker.cross{stroke:#333333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 p{margin:0;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster-label text{fill:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster-label span{color:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster-label span p{background-color:transparent;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .label text,#mermaid-svg-DCaNG4sx3y4ZCXo3 span{fill:#333;color:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .node rect,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node circle,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node ellipse,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node polygon,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .rough-node .label text,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node .label text,#mermaid-svg-DCaNG4sx3y4ZCXo3 .image-shape .label,#mermaid-svg-DCaNG4sx3y4ZCXo3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .rough-node .label,#mermaid-svg-DCaNG4sx3y4ZCXo3 .node .label,#mermaid-svg-DCaNG4sx3y4ZCXo3 .image-shape .label,#mermaid-svg-DCaNG4sx3y4ZCXo3 .icon-shape .label{text-align:center;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .node.clickable{cursor:pointer;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .arrowheadPath{fill:#333333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DCaNG4sx3y4ZCXo3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DCaNG4sx3y4ZCXo3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster text{fill:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .cluster span{color:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DCaNG4sx3y4ZCXo3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .icon-shape,#mermaid-svg-DCaNG4sx3y4ZCXo3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .icon-shape p,#mermaid-svg-DCaNG4sx3y4ZCXo3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .icon-shape .label rect,#mermaid-svg-DCaNG4sx3y4ZCXo3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DCaNG4sx3y4ZCXo3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DCaNG4sx3y4ZCXo3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DCaNG4sx3y4ZCXo3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTML/文件/媒体类型
JSON 字段结构
业务错误状态码
我要控制响应
控制什么
response_class 或 Response 对象
response_model
HTTPException
5. HTTPException,主动返回业务错误
FastAPI 自动校验失败会返回 422。
但业务错误需要你主动抛出。
比如新闻不存在:
python
from fastapi import HTTPException
@app.get("/news/{news_id}")
async def get_news(news_id: int):
news = None
if news is None:
raise HTTPException(status_code=404, detail="新闻不存在")
return news
常见状态码:
| 状态码 | 含义 |
|---|---|
| 400 | 请求语义有问题 |
| 401 | 未认证 |
| 403 | 已认证但无权限 |
| 404 | 资源不存在 |
| 422 | 参数校验失败 |
| 500 | 服务端内部错误 |
课程里更新图书、删除图书时都会先查对象,查不到就抛 404。
这个习惯很好。
因为接口调用方需要知道,是删除成功了,还是目标本来就不存在。
6. 在 AI 掘金头条项目里的响应设计
项目里引入了统一响应格式:
json
{
"code": 200,
"message": "操作成功",
"data": {}
}
对应工具函数大概是:
python
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
def success_response(message="操作成功", data=None, code=200):
return JSONResponse(
content=jsonable_encoder({
"code": code,
"message": message,
"data": data
})
)
这样做的好处是,前端处理接口返回值更稳定。
但要注意,统一响应格式不等于可以放弃 HTTP 状态码。
比较合理的方式是:
- 成功响应,使用统一结构
- 业务异常,使用
HTTPException或全局异常处理器统一包装 - 数据结构,继续用 Pydantic schema 约束
不要让所有错误都返回 HTTP 200,然后只靠 code 字段表达失败。
那样前端、网关、监控系统都会变得难受。
7. Pydantic v2 下的响应模型
项目里使用了 Pydantic v2 风格:
python
from pydantic import BaseModel, ConfigDict
class NewsItemBase(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
title: str
from_attributes=True 的作用是,允许 Pydantic 从 ORM 对象属性中读取字段。
比如 SQLAlchemy 返回的是 News 对象,不是普通 dict。
有了这个配置,就可以更自然地把 ORM 对象转成响应模型。
python
response = NewsItemBase.model_validate(news)
这也是 Pydantic v2 里很常见的写法。
8. 常见坑
坑 1,误以为 response_model 只是文档
不是。它会参与输出序列化和字段过滤。
坑 2,把数据库对象原样返回
学习阶段可以,真实项目要谨慎。数据库字段不等于 API 字段。
坑 3,把密码哈希、Token、手机号等敏感字段塞进响应
响应模型就是用来防这类问题的。
坑 4,把 response_class 和 response_model 混用
它们可以同时出现,但各管各的。一个管 HTTP 响应类型,一个管 JSON schema。
坑 5,文件路径写死相对路径
开发时能跑,上线换工作目录就可能找不到。
9. 小结
FastAPI 响应系统可以这样记:
text
普通 API,直接返回 dict/list/model
想返回 HTML 或文件,用 response_class 或 Response 对象
想限制 JSON 字段,用 response_model
想主动表达业务错误,用 HTTPException
如果说参数系统解决的是请求怎么进来,那么响应系统解决的就是数据怎么出去。
一个靠谱的 API,不只要能返回数据,还要知道哪些数据不该返回。
下一篇我们继续进入工程化,讲中间件和依赖注入。