创建时间: 2026-08-25最后更新: 2026-08-27

1. 响应

路径函数完成后,FastAPI 需要把 Python 结果转换成 HTTP 响应。一个响应不只有 JSON 数据,还包括状态码、响应头和媒体类型:

response.txt
1
HTTP/1.1 200 OK
2
Content-Type: application/json
3
4
{"answer":"FastAPI 是一个 Python Web 框架。"}

FastAPI 默认很适合 JSON API。返回字典、列表或 Pydantic 模型时,它会把结果编码成 JSON,并设置合适的 Content-Type。

本文的完整可请求案例位于 src/fastapi/api/res/,每个下划线命名的 Python 文件对应一个独立路由模块;它们统一由 res/__init__.py 聚合。文章中的说明性表格、HTTP 报文和文字解释不对应项目文件。

res/__init__.py 负责声明响应案例的领域前缀,并聚合各个案例。上层 api/router.py 将整个响应案例模块挂载到 /api/v1:

res/__init__.py
api/router.py
01
from fastapi import APIRouter
02
03
from . import (
04
create_response,
05
direct_json_response,
06
documented_responses,
07
dynamic_status,
08
exclude_unset,
09
file_response,
10
no_content,
11
response_classes,
12
response_cookie,
13
response_header,
14
response_model,
15
return_model,
16
serialization,
17
)
18
19
# /api/v1 由上层聚合路由统一添加,本模块只声明自己的领域前缀。
20
router = APIRouter(prefix="/res", tags=["responses"])
21
22
router.include_router(serialization.router)
23
router.include_router(response_model.router)
24
router.include_router(return_model.router)
25
router.include_router(create_response.router)
26
router.include_router(dynamic_status.router)
27
router.include_router(exclude_unset.router)
28
router.include_router(response_header.router)
29
router.include_router(response_cookie.router)
30
router.include_router(response_classes.router)
31
router.include_router(direct_json_response.router)
32
router.include_router(file_response.router)
33
router.include_router(no_content.router)
34
router.include_router(documented_responses.router)

因此,案例文件中的相对路径会统一组合成 /api/v1/res/...。

2. 序列化

JSON 只支持有限的数据类型,但 Python 还包含日期、UUID 和模型对象。FastAPI 会根据路径函数有没有声明响应模型,选择不同的序列化方式:

返回方式FastAPI 的处理
声明 response_model 或有效的返回类型使用 Pydantic 校验、过滤并序列化响应
没有声明响应模型使用 jsonable_encoder 转换,再交给 JSONResponse
直接返回 Response 子类原样发送,不再自动校验和转换内容

我们先来看没有显式响应模型时最常见的写法:

下面的案例请求一个固定 UUID 对应的会话,并同时展示正文、状态码、响应头和响应 URL:

预览
本地服务:心跳检测中
serialization.py
01
from datetime import datetime, timezone
02
from uuid import UUID
03
04
from fastapi import APIRouter
05
06
router = APIRouter()
07
08
@router.get("/conversations/{conversation_id}")
09
async def get_conversation(conversation_id: UUID) -> dict[str, UUID | datetime]:
10
return {
11
"id": conversation_id,
12
"created_at": datetime.now(timezone.utc),
13
}

UUID 和时间会转换成 JSON 字符串。时间字段应明确时区,跨系统传输时通常使用 UTC 和 ISO 8601 格式,不要返回依赖服务器本地时区的模糊时间。

这个案例没有声明响应模型,所以 FastAPI 会先把 UUID 和时间转换成 JSON 能够表示的数据。如果接口声明了 Pydantic 响应模型,FastAPI 会直接使用 Pydantic 完成响应校验和序列化,不需要在路径函数中手动调用 model_dump() 或 jsonable_encoder()。

3. 响应模型

使用 response_model 可以声明并校验公开响应:

下面的案例可以直接观察公开响应中只有 id 和 name,服务端返回的 password_hash 会被响应模型过滤:

预览
本地服务:心跳检测中
response_model.py
01
from fastapi import APIRouter
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
class UserRead(BaseModel):
07
id: str
08
name: str
09
10
@router.get("/users/{user_id}", response_model=UserRead)
11
async def get_user(user_id: str) -> dict[str, str]:
12
return {
13
"id": user_id,
14
"name": "小微",
15
"password_hash": "internal-value",
16
}

响应中只会保留 UserRead 声明的字段,password_hash 不会发送给客户端。这种过滤是重要的安全边界,但不能代替正确的数据分层:业务代码也不应该随意把敏感对象交给响应层。

可以直接把返回类型写成 Pydantic 模型:

这个案例请求使用返回类型声明响应模型的接口,它与上一个案例采用不同声明方式,但都会经过响应校验和序列化:

预览
本地服务:心跳检测中
return_model.py
01
from fastapi import APIRouter
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
class UserRead(BaseModel):
07
id: str
08
name: str
09
10
@router.get("/typed-users/{user_id}")
11
async def get_user(user_id: str) -> UserRead:
12
return UserRead(id=user_id, name="小微")

返回类型和 response_model 都能参与运行时响应校验、序列化、字段过滤和文档生成。它们不是只给编辑器看的类型提示。

两者同时存在时,response_model 优先。一般可以按下面的方式选择:

情况推荐写法
返回值本身就是公开模型使用返回类型 -> UserRead
函数返回字典或内部对象,但公开契约不同使用 response_model=UserRead
直接返回 Response 子类返回类型写 Response 或具体子类
类型只供 Python 工具使用,不希望生成响应模型设置 response_model=None

响应数据不符合模型时,通常意味着服务端实现违反了自己的契约。FastAPI 会把它作为服务器错误处理,而不是向客户端返回 422。422 主要表示客户端提交的请求无法通过校验。

4. 输入输出

创建资源时,输入和输出应使用不同模型:

下面的案例会提交会话标题。请求成功后,可以在响应元数据中观察 201 状态码以及浏览器允许读取的响应头:

预览
本地服务:心跳检测中
create_response.py
01
from datetime import datetime, timezone
02
from uuid import uuid4
03
04
from fastapi import APIRouter, Response, status
05
from pydantic import BaseModel
06
07
router = APIRouter()
08
09
10
class ConversationCreate(BaseModel):
11
title: str
12
13
14
class ConversationRead(BaseModel):
15
id: str
16
title: str
17
created_at: datetime
18
19
20
@router.post(
21
"/conversations",
22
response_model=ConversationRead,
23
status_code=status.HTTP_201_CREATED,
24
)
25
async def create_conversation(
26
payload: ConversationCreate,
27
response: Response,
28
) -> ConversationRead:
29
conversation_id = str(uuid4())
30
response.headers["Location"] = f"/api/v1/res/conversations/{conversation_id}"
31
32
return ConversationRead(
33
id=conversation_id,
34
title=payload.title,
35
created_at=datetime.now(timezone.utc),
36
)

客户端只提供标题,服务器负责生成 ID 和时间。响应模型把这种所有权写进了接口契约。201 Created 表示服务器创建了新资源,Location 响应头进一步告诉客户端可以从哪里访问它。

5. 状态码

固定状态码写在路径装饰器中,动态状态码可以通过 Response 参数调整:

第一次提交新的 item_id 时接口返回 201,再次提交同一个 ID 时返回 200。可以修改 ID 和查询参数 value 观察状态变化:

预览
本地服务:心跳检测中
dynamic_status.py
01
from fastapi import APIRouter, Response, status
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
07
class ItemRead(BaseModel):
08
id: str
09
value: str
10
11
12
items: dict[str, str] = {}
13
14
15
@router.put(
16
"/items/{item_id}",
17
response_model=ItemRead,
18
responses={
19
201: {
20
"model": ItemRead,
21
"description": "创建了新资源",
22
},
23
},
24
)
25
async def upsert_item(
26
item_id: str,
27
value: str,
28
response: Response,
29
) -> dict[str, str]:
30
created = item_id not in items
31
items[item_id] = value
32
33
if created:
34
response.status_code = status.HTTP_201_CREATED
35
response.headers["Location"] = f"/api/v1/res/items/{item_id}"
36
37
return {"id": item_id, "value": value}

只有业务结果确实存在多种成功状态时才动态修改。接口行为越稳定,客户端越容易处理。

这里还要区分实际响应和接口文档。给注入的 Response 设置 status_code 会改变本次请求的实际状态码,但 OpenAPI 无法根据函数中的条件分支自动推断 201,因此还需要使用装饰器的 responses 参数声明这个分支。responses 只补充文档,不会替我们执行状态切换。

常见的成功状态码可以这样理解:

状态码含义响应正文
200 OK请求成功并返回结果通常有
201 Created已经创建新资源通常返回资源,并可提供 Location
202 Accepted已接收任务,但尚未处理完成通常返回任务标识或查询地址
204 No Content请求成功,但没有内容需要返回不能有
304 Not Modified客户端缓存仍然有效不能有

6. 默认字段

响应模型可以控制默认字段是否输出。例如数据记录中有许多可选字段时,可以只返回实际赋值的内容:

下面的响应只包含服务端实际设置的 name,默认字段不会出现在 data 中:

预览
本地服务:心跳检测中
exclude_unset.py
01
from fastapi import APIRouter
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
07
class UserProfile(BaseModel):
08
name: str
09
bio: str | None = None
10
theme: str = "system"
11
12
13
@router.get(
14
"/profiles/{user_id}",
15
response_model=UserProfile,
16
response_model_exclude_unset=True,
17
)
18
async def get_profile(user_id: str) -> dict[str, str]:
19
return {"name": "小微"}

响应只包含 name,因为 bio 和 theme 没有被实际设置。这里判断的是字段是否被显式赋值:即使客户端明确传入 bio=None 或 theme="system",值刚好与默认值相同,response_model_exclude_unset=True 仍会保留这些字段。

FastAPI 还提供两个容易混淆的选项:

参数排除规则
response_model_exclude_unset排除没有显式赋值的字段
response_model_exclude_defaults排除值等于字段默认值的字段
response_model_exclude_none排除值为 None 的字段

此外,response_model_include 和 response_model_exclude 可以临时选择部分字段,但 OpenAPI 仍然会引用完整模型的 Schema,文档和实际响应容易产生理解偏差。公开接口长期需要不同字段时,优先创建独立的响应模型。

这些选项适合稀疏数据,但也可能让同一接口的字段忽隐忽现。稳定公开契约通常更容易使用,不要只为了减少几个字节就默认开启。

7. 响应头

FastAPI 可以注入 Response,在保留普通返回值和响应模型能力的同时设置响应头:

下面的案例会请求健康状态,并把浏览器能够读取的响应头一起显示出来:

预览
本地服务:心跳检测中
response_header.py
1
from fastapi import APIRouter, Response
2
3
router = APIRouter()
4
5
6
@router.get("/response-health")
7
async def health(response: Response) -> dict[str, str]:
8
response.headers["X-Service-Version"] = "0.1.0"
9
return {"status": "ok"}

分页链接、缓存策略、限流信息和 Request ID 都可能放在响应头中。浏览器跨域读取自定义响应头时,还要在 CORS 配置中通过 expose_headers 明确暴露。

响应对象也可以设置 Cookie:

下面的请求允许浏览器接收凭证,用于观察设置 Cookie 的接口行为:

预览
本地服务:心跳检测中
response_cookie.py
01
from fastapi import APIRouter, Response
02
03
router = APIRouter()
04
05
06
@router.post("/sessions")
07
async def create_session(response: Response) -> dict[str, bool]:
08
response.set_cookie(
09
key="session_id",
10
value="signed-session-value",
11
httponly=True,
12
secure=True,
13
samesite="lax",
14
max_age=3600,
15
)
16
return {"created": True}

httponly 可以阻止前端 JavaScript 读取 Cookie,secure 表示只通过 HTTPS 发送,samesite 控制跨站请求携带规则。真正的会话值还需要安全随机、签名或服务端存储,不能直接相信客户端传回的任意文本。

9. 响应类

不是所有响应都是 JSON。FastAPI 提供多种 Response 类:

下面两个案例分别请求纯文本和重定向响应。重定向请求会记录原始地址和浏览器自动跟随后到达的最终地址:

预览
本地服务:心跳检测中
预览
本地服务:心跳检测中
response_classes.py
01
from fastapi import APIRouter
02
from fastapi.responses import PlainTextResponse, RedirectResponse
03
04
router = APIRouter()
05
06
07
@router.get("/robots.txt", response_class=PlainTextResponse)
08
async def robots() -> str:
09
return "User-agent: *\nDisallow: /"
10
11
12
@router.get("/documentation")
13
async def documentation() -> RedirectResponse:
14
return RedirectResponse(url="/docs")

RedirectResponse 默认使用 307,它会要求客户端在跳转后保留原请求方法。如果业务需要永久跳转或希望改变请求方法,应根据协议显式选择 301、302、303、307 或 308,而不是默认把所有重定向都理解成 302。

常见类型包括:

类型用途
JSONResponse手动控制 JSON 响应
PlainTextResponse纯文本
HTMLResponseHTML 内容
FileResponse发送磁盘文件
RedirectResponseHTTP 重定向
StreamingResponse持续输出数据流

直接返回一个 Response 对象时,开发者接管了状态码、内容和媒体类型。FastAPI 不会再按照普通 Pydantic 响应模型处理内容,因此要自己保证格式与文档一致。

这里不要把注入的 Response 参数和直接返回的 Response 对象混为一谈。前者是一个临时响应对象,FastAPI 会提取其中的状态码、响应头和 Cookie,再继续处理路径函数返回的数据;后者已经是最终响应,FastAPI 会将它原样发送。

response_class=PlainTextResponse 会告诉 FastAPI 如何构造响应,并把媒体类型写入 OpenAPI;return PlainTextResponse(...) 则直接返回已经构造好的对象。需要动态状态码或完全控制内容时使用后者,固定格式优先使用装饰器参数。

如果先用 response_model 约束数据,又直接返回 JSONResponse,不要期待响应模型继续过滤内容。直接 Response 是一条绕过自动响应处理的通道。

直接构造 JSONResponse 时,内容必须已经能够被 JSON 编码。datetime、UUID、Pydantic 模型等对象需要先转换:

这个案例用于观察手动构造的 JSON 响应以及时间字段最终得到的字符串形式:

预览
本地服务:心跳检测中
direct_json_response.py
01
from datetime import datetime, timezone
02
03
from fastapi import APIRouter
04
from fastapi.encoders import jsonable_encoder
05
from fastapi.responses import JSONResponse
06
07
router = APIRouter()
08
09
10
@router.get("/build-info")
11
async def get_build_info() -> JSONResponse:
12
content = {
13
"version": "0.1.0",
14
"generated_at": datetime.now(timezone.utc),
15
}
16
return JSONResponse(content=jsonable_encoder(content))

如果已经声明了 Pydantic 响应模型,就不需要手动做这一步。只有确实需要自己控制响应对象时,才接管这部分转换工作。

文件下载则可以使用 FileResponse:

下面的案例以 Blob 接收 PDF,只展示响应状态、文件名、媒体类型和文件大小:

预览
本地服务:心跳检测中
file_response.py
01
from pathlib import Path
02
03
from fastapi import APIRouter
04
from fastapi.responses import FileResponse
05
06
router = APIRouter()
07
report_path = Path(__file__).parent / "files" / "report.pdf"
08
09
10
@router.get("/reports/latest", response_class=FileResponse)
11
async def download_report() -> FileResponse:
12
return FileResponse(
13
path=report_path,
14
media_type="application/pdf",
15
filename="fastapi-report.pdf",
16
)

media_type 决定 Content-Type,filename 会让 FileResponse 生成用于下载的 Content-Disposition。文件不存在、路径是否允许访问等检查仍应由业务代码负责,不能直接把客户端传入的任意路径交给 FileResponse。

10. 空响应

删除资源后可以返回 204:

下面的案例发送 DELETE 请求。成功后可以看到 204 状态码,并确认响应中没有正文:

预览
本地服务:心跳检测中
no_content.py
01
from fastapi import APIRouter, Response, status
02
03
router = APIRouter()
04
05
06
@router.delete(
07
"/conversations/{conversation_id}",
08
status_code=status.HTTP_204_NO_CONTENT,
09
)
10
async def delete_conversation(conversation_id: str) -> Response:
11
return Response(status_code=status.HTTP_204_NO_CONTENT)

204 表示响应没有正文,不应再返回 {"success": true}。如果客户端确实需要删除结果或新状态,就使用 200 并声明相应模型。

11. 其他响应

一个接口可能除了成功响应,还会返回业务错误。可以通过装饰器的 responses 参数把这些分支写入 OpenAPI:

下面两个案例请求同一个接口:第一个得到正常会话,第二个使用不存在的 ID 得到文档中声明的 404 错误响应:

预览
本地服务:心跳检测中
预览
本地服务:心跳检测中
documented_responses.py
01
from fastapi import APIRouter
02
from fastapi.responses import JSONResponse
03
from pydantic import BaseModel
04
05
router = APIRouter()
06
07
08
class ConversationRead(BaseModel):
09
id: str
10
title: str
11
12
13
class ErrorResponse(BaseModel):
14
code: str
15
message: str
16
17
18
conversations = {
19
"conversation-1": {"title": "新会话"},
20
}
21
22
23
@router.get(
24
"/documented-conversations/{conversation_id}",
25
response_model=ConversationRead,
26
responses={
27
404: {
28
"model": ErrorResponse,
29
"description": "会话不存在",
30
},
31
},
32
)
33
async def get_conversation(
34
conversation_id: str,
35
) -> dict[str, str] | JSONResponse:
36
conversation = conversations.get(conversation_id)
37
38
if conversation is None:
39
return JSONResponse(
40
status_code=404,
41
content={
42
"code": "conversation_not_found",
43
"message": "会话不存在",
44
},
45
)
46
47
return {
48
"id": conversation_id,
49
"title": conversation["title"],
50
}

responses 主要补充文档,并不会自动把所有异常转换成这个结构。这里仍然需要在失败分支中明确返回 404;实际项目也可以由下一篇介绍的异常处理器统一生成错误响应。

除了 model 和 description,responses 还可以声明不同媒体类型、示例、响应头和 OpenAPI links。它也能为主成功响应补充说明,并与 response_model、status_code 合并生成最终文档。不过这些内容仍然只是契约描述,运行时代码必须返回与契约一致的状态码、媒体类型和数据结构。

12. 契约

响应模型应该面向调用方,而不是简单映射内部对象:

  • 只公开客户端真正需要的字段;
  • 时间、枚举和可空字段保持稳定格式;
  • 敏感字段不进入公开模型;
  • 列表接口考虑分页信息;
  • 错误响应使用独立的错误契约;
  • 不要频繁改变同一字段的数据类型。

对于 AI 接口,普通响应可以返回消息 ID、会话 ID、内容、模型标识和结束原因。供应商返回的完整原始对象不应未经筛选直接暴露给网页。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容