首页
/ FastAPI 返回额外状态码:直接返回 JSONResponse 实现 200/201 双分支响应

FastAPI 返回额外状态码:直接返回 JSONResponse 实现 200/201 双分支响应

2026-09-06 14:03:53作者:蔡怀权

本文基于 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/foofoo 已存在于 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)

(见 routing.py#L706-L747

  • 若返回值是 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_codefastapi/utils.py)——对于 204、304 这类不允许响应体的状态码,会自动把 body 置空;而直接返回 Response 的分支不做此处理,body 完全由你控制。这既体现了灵活性,也再次印证了官方 warning:框架在这一侧不做任何兜底校验。

技术细节:fastapi.responses 与 fastapi.status 来自 Starlette

官方文档的 “Technical Details” 备注指出:你也可以写 from starlette.responses import JSONResponse

从源码看,fastapi/responses.py 中的 ResponseJSONResponseHTMLResponsePlainTextResponseRedirectResponseFileResponseStreamingResponse 等全部是 from starlette.responses import ... 的直接再导出,FastAPI 只是把 Starlette 的响应类重新暴露出来方便开发者使用。同理,fastapi/init.py 中有一行 from starlette import status as status,所以 from fastapi import statusfrom starlette import status 指向同一个命名空间,status.HTTP_201_CREATEDstatus.HTTP_200_OK 等常量都来自 Starlette。

另外结合 fastapi/responses.py 可以看到:UJSONResponseORJSONResponse 已被标记为 deprecated(FastAPI 现在在设置了返回类型或 response model 时会通过 Pydantic 直接序列化到 JSON 字节)。因此在“直接返回 JSONResponse 指定额外状态码”这一模式下,标准库序列化能力的 JSONResponse 就是最稳妥、无额外依赖的选择。

OpenAPI 与 API 文档:额外状态码如何记录

由于额外状态码的 JSONResponse 是在运行时按分支决定的,FastAPI 无法预先知道你会返回什么,因此这些直接返回的额外状态码不会自动出现在 OpenAPI Schema(API 文档)中。

官方文档给出的解决方案是使用 Additional Responses 章节介绍的路径操作装饰器 responses 参数,把这些响应声明进 OpenAPI。其使用方式(摘自该章节):

  • responses 接收一个 dict:键是状态码(如 "200""404"),值是包含该响应信息的 dict,可含 descriptionheaderscontent 等 OpenAPI Response 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.responsesfastapi.status 本质上是 Starlette 的再导出,两处导入可互换;
  • 额外状态码不会自动进入 OpenAPI,需要配合 responses 参数(additional-responses 文档)在代码中显式声明,使 API 文档与实际行为保持一致。
登录后查看全文
热门项目推荐
相关项目推荐