FastAPI 响应系统,response_class 和 response_model 不是一回事

写 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 数据结构
解决问题 客户端如何理解响应 接口暴露哪些字段
常见值 HTMLResponseFileResponsePlainTextResponse 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,不只要能返回数据,还要知道哪些数据不该返回。

下一篇我们继续进入工程化,讲中间件和依赖注入。

参考资料