FastAPI 返回额外状态码:直接返回 JSONResponse 实现 200/201 双分支响应
本文基于 FastAPI 官方文档《Additional Status Codes》及其配套源码展开,讲解如何在同一路径操作中返回不同 HTTP 状态码(例如更新已存在资源返回 200、创建新资源返回 201):核心手段是直接返回 JSONResponse 并手动指定 status_code。读完后你将掌握这一技巧的完整可运行代码、官方警告的使用边界,以及从 FastAPI 源码层面理解“为什么直接返回 Response 会跳过序列化流水线”,并知道如何为这些额外状态码补充 OpenAPI 文档。
默认行为:JSONResponse 与路径操作的状态码
按默认机制,FastAPI 使用 JSONResponse 返回响应:路径操作(path operation)返回的内容会被放入 JSONResponse 中,状态码则取你在路径操作装饰器中设置的值(未设置时为默认状态码)。
也就是说,一个典型的 @app.put(...) 只要返回普通 Python 对象(dict、Pydantic 模型等),FastAPI 会替你完成序列化和响应封装,你不需要关心 JSONResponse。但当同一个操作需要按不同分支返回不同状态码(而不是固定的 200)时,就需要手动介入。
实战:更新或创建(upsert)并返回 200 / 201
官方文档给出的场景是:一个允许更新条目(items)的路径操作,更新成功时返回 200 OK;同时它也要接受新条目——当条目此前不存在时创建它,并返回 201 Created。
实现方式是:导入 JSONResponse,在“创建”分支中直接返回它,并通过 status_code 参数指定你想要的状态码。官方示例代码(仓库中 tutorial001_an_py310.py)完整如下:
from typing import Annotated
from fastapi import Body, FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}}
@app.put("/items/{item_id}")
async def upsert_item(
item_id: str,
name: Annotated[str | None, Body()] = None,
size: Annotated[int | None, Body()] = None,
):
if item_id in items:
item = items[item_id]
item["name"] = name
item["size"] = size
return item
else:
item = {"name": name, "size": size}
items[item_id] = item
return JSONResponse(status_code=status.HTTP_201_CREATED, content=item)
代码要点拆解:
- 更新分支(
item_id in items):直接return item返回dict,走 FastAPI 默认的JSONResponse序列化,状态码为默认的200。 - 创建分支:构造新条目后,
return JSONResponse(status_code=status.HTTP_201_CREATED, content=item)——状态码在JSONResponse构造时直接指定,content就是 JSON 负载。 status.HTTP_201_CREATED是常量,等价于整数201,从fastapi顶层直接导入(见后文技术细节)。
仓库中同时提供了不使用 Annotated 的等价写法(tutorial001_py310.py),仅参数声明风格不同:
name: str | None = Body(default=None),
size: int | None = Body(default=None),
两个版本的响应行为完全一致,可用 test_tutorial001.py 中的测试断言来验证:
def test_update(client: TestClient):
response = client.put("/items/foo", json={"name": "Wrestlers"})
assert response.status_code == 200, response.text
assert response.json() == {"name": "Wrestlers", "size": None}
def test_create(client: TestClient):
response = client.put("/items/red", json={"name": "Chillies"})
assert response.status_code == 201, response.text
assert response.json() == {"name": "Chillies", "size": None}
PUT /items/foo(foo已存在于items)→200,返回更新后的数据;PUT /items/red(新 id)→201,返回新建的数据。
官方警告:直接返回 Response 不会被模型序列化
官方文档在此处给出了明确的 warning,务必理解其边界:
当你直接返回一个
Response(如上例的JSONResponse)时,它会原样直接返回。它不会被任何模型序列化(serialize with a model),FastAPI 也不做response_model过滤。请确保其中的数据是你真正想要返回的数据,且(如果使用JSONResponse)值是合法的 JSON。
换句话说,response_model、别名(by_alias)、include/exclude 等模型级处理在这条路径上全部失效,响应内容的正确性由你自己负责。
源码深潜:直接返回的 Response 为何绕过序列化流水线
上述警告的根源可以在 FastAPI 的核心路由代码 fastapi/routing.py 中找到。路径操作执行完毕后,框架对返回值做了二分判断:
raw_response = await run_endpoint_function(
dependant=dependant,
values=solved_result.values,
is_coroutine=is_coroutine,
)
if isinstance(raw_response, Response):
if raw_response.background is None:
raw_response.background = solved_result.background_tasks
response = raw_response
else:
response_args = _build_response_args(
status_code=status_code, solved_result=solved_result
)
...
content = await serialize_response(...)
...
response = actual_response_class(content, **response_args)
- 若返回值是
Response的实例(包括JSONResponse),FastAPI 仅在background为空时补上依赖中声明的后台任务,然后直接采用该对象作为最终响应——状态码、Content-Type、body 都以你构造时的为准; - 否则才会进入
serialize_response,按response_field/response_model做验证与序列化,再交给actual_response_class(默认即JSONResponse)封装,此时状态码由_build_response_args中的status_code决定(即装饰器参数或默认值)。
还有一个值得注意的细节:在非直接返回的分支里,框架会调用 is_body_allowed_for_status_code(fastapi/utils.py)——对于 204、304 这类不允许响应体的状态码,会自动把 body 置空;而直接返回 Response 的分支不做此处理,body 完全由你控制。这既体现了灵活性,也再次印证了官方 warning:框架在这一侧不做任何兜底校验。
技术细节:fastapi.responses 与 fastapi.status 来自 Starlette
官方文档的 “Technical Details” 备注指出:你也可以写 from starlette.responses import JSONResponse。
从源码看,fastapi/responses.py 中的 Response、JSONResponse、HTMLResponse、PlainTextResponse、RedirectResponse、FileResponse、StreamingResponse 等全部是 from starlette.responses import ... 的直接再导出,FastAPI 只是把 Starlette 的响应类重新暴露出来方便开发者使用。同理,fastapi/init.py 中有一行 from starlette import status as status,所以 from fastapi import status 与 from starlette import status 指向同一个命名空间,status.HTTP_201_CREATED、status.HTTP_200_OK 等常量都来自 Starlette。
另外结合 fastapi/responses.py 可以看到:UJSONResponse 与 ORJSONResponse 已被标记为 deprecated(FastAPI 现在在设置了返回类型或 response model 时会通过 Pydantic 直接序列化到 JSON 字节)。因此在“直接返回 JSONResponse 指定额外状态码”这一模式下,标准库序列化能力的 JSONResponse 就是最稳妥、无额外依赖的选择。
OpenAPI 与 API 文档:额外状态码如何记录
由于额外状态码的 JSONResponse 是在运行时按分支决定的,FastAPI 无法预先知道你会返回什么,因此这些直接返回的额外状态码不会自动出现在 OpenAPI Schema(API 文档)中。
官方文档给出的解决方案是使用 Additional Responses 章节介绍的路径操作装饰器 responses 参数,把这些响应声明进 OpenAPI。其使用方式(摘自该章节):
responses接收一个dict:键是状态码(如"200"、"404"),值是包含该响应信息的dict,可含description、headers、content等 OpenAPIResponse Object允许的字段;- 每个响应
dict可以带一个model键(一个 Pydantic 模型),FastAPI 会为其生成 JSON Schema 并以$ref形式挂到 OpenAPI 的components/schemas中; - 注意:声明了额外响应后,运行时仍必须自己直接返回对应的
JSONResponse(或其他Response),声明只影响文档,不影响实际响应行为; - 还可以用
**dict解包技巧把多个路径操作共用的预定义响应与单个操作特有的响应组合复用。
这样,“实际响应”(直接返回 JSONResponse)与“OpenAPI 契约”(responses 参数)两侧对齐,客户端和文档看到的都是一致的 200/201(或更多)响应集合。
快速验证
无需启动浏览器即可用 FastAPI 自带的 TestClient 复现本文行为,对应仓库测试 test_tutorial001.py 的思路:
from fastapi.testclient import TestClient
from docs_src.additional_status_codes.tutorial001_an_py310 import app
client = TestClient(app)
# 已存在条目 -> 200
r = client.put("/items/foo", json={"name": "Wrestlers"})
assert r.status_code == 200 and r.json() == {"name": "Wrestlers", "size": None}
# 新条目 -> 201
r = client.put("/items/red", json={"name": "Chillies"})
assert r.status_code == 201 and r.json() == {"name": "Chillies", "size": None}
也可将示例模块保存后用 uvicorn 运行,在 /docs 中确认:默认文档只展示 200(及校验错误 422)响应,201 需要按上文用 responses 参数显式声明后才会出现。
小结
- 要返回额外状态码,直接
return JSONResponse(status_code=..., content=...)是最直接的方式,适用于 upsert(200/201)这类分支场景; - 直接返回的
Response会被 FastAPI 原样送出(fastapi/routing.py),不经过response_model序列化,内容合法性需自行保证; fastapi.responses与fastapi.status本质上是 Starlette 的再导出,两处导入可互换;- 额外状态码不会自动进入 OpenAPI,需要配合
responses参数(additional-responses 文档)在代码中显式声明,使 API 文档与实际行为保持一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00