FastAPI 请求体更新实战:用 PUT 替换、用 PATCH 部分更新(Body - Updates 指南)
2026/9/10 12:51:17 网站建设 项目流程

FastAPI 请求体更新实战:用 PUT 替换、用 PATCH 部分更新(Body - Updates 指南)

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

导读

更新资源是 Web API 最常用的操作之一,而"整包替换"与"部分更新"是两种截然不同的语义。本篇指南基于 FastAPI 官方教程的 Body - Updates 章节(韩语版位于 docs/ko/docs/tutorial/body-updates.md),完整讲解如何用 HTTPPUT做替换式更新、如何用PATCH配合 Pydantic 的exclude_unsetmodel_copy(update=...)实现精确的部分更新,并结合仓库源码与测试用例深入剖析jsonable_encoder的底层行为。读完你将掌握一套可直接落地的"更新端点"写法,并理解为什么部分更新必须显式排除默认值。

更新操作的两大语义:PUT替换 vsPATCH部分更新

在动手写代码之前,先厘清两个 HTTP 方法的设计意图:

  • PUT:接收的请求体代表资源的完整新状态,语义上是"用这份数据替换掉现有数据"。
  • PATCH:接收的请求体只包含需要变更的字段,语义上是"对现有数据做部分修改,其余保持不变"。

FastAPI 本身不强制你遵守哪种约定——正如教程注释所强调的:很多团队即使做部分更新也只使用PUT,你完全可以按自己的偏好选择,FastAPI 不会施加任何限制;本指南展示的是这两种方法各自被设计出来的典型用法

PUT做替换式更新

完整示例代码

仓库中对应的可运行示例为 docs_src/body_updates/tutorial001_py310.py(Python 3.10+ 语法,使用str | None联合类型写法):

from fastapi import FastAPI from fastapi.encoders import jsonable_encoder from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str | None = None description: str | None = None price: float | None = None tax: float = 10.5 tags: list[str] = [] items = { "foo": {"name": "Foo", "price": 50.2}, "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, } @app.get("/items/{item_id}", response_model=Item) async def read_item(item_id: str): return items[item_id] @app.put("/items/{item_id}", response_model=Item) async def update_item(item_id: str, item: Item): update_item_encoded = jsonable_encoder(item) items[item_id] = update_item_encoded return update_item_encoded

这段代码的核心步骤只有三步:

  1. 用路径参数item_id定位要更新的资源;
  2. 通过jsonable_encoder(item)把 Pydantic 模型转换为"可存储为 JSON"的普通数据结构;
  3. 写回存储(示例中是内存字典items,真实项目里可替换为 NoSQL 数据库的put/set操作),并返回更新后的数据。

jsonable_encoder是什么?为什么存储前需要它?

jsonable_encoder是 FastAPI 内部广泛使用的工具函数,定义在 fastapi/encoders.py。它的职责是:把任意 Python 对象转换为可以被 JSON 序列化的形式。FastAPI 在把所有响应发送给客户端之前都会先经过它,你同样可以在自己的代码里调用它——比如在把对象写入"只支持 JSON 的数据库"(如 MongoDB、Redis 等 NoSQL 场景)之前。

为什么要做这一步转换?因为 Pydantic 模型里可能含有 JSON 原生不认识的类型。从源码 fastapi/encoders.py 中ENCODERS_BY_TYPE这张映射表可以看到它处理的主要类型:

Python 类型编码结果说明
datetime.date/datetime.datetime/datetime.timeisoformat()字符串例如2026-09-08T21:07:17
datetime.timedeltatotal_seconds()浮点数时长转为秒数
Decimalintfloat指数非负时转int,否则转float
Enumo.value取枚举成员的值
set/frozenset/deque/GeneratorTypelist集合类转列表
bytes解码后的字符串o.decode()
UUID字符串转为标准 UUID 文本
IPv4Address/IPv6Address字符串网络地址转文本
Path字符串路径转文本
Color字符串颜色转文本

此外,jsonable_encoder还透传了 Pydantic 的includeexcludeby_aliasexclude_unsetexclude_defaultsexclude_nonecustom_encoder等参数,并默认开启sqlalchemy_safe=True,用于过滤 SQLAlchemy 对象内部以_sa开头的私有属性(它们不应被序列化)。

替换式更新的"坑":默认值会覆盖已存储的数据

PUT的替换语义带来一个容易被忽视的问题。假设用下面的请求体更新已存在的bar条目:

{ "name": "Barz", "price": 3, "description": None, }

bar原来存储的数据是{"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}——其中tax是 20.2。

问题在于:请求体里没有tax字段,而 Pydantic 的Item模型给tax定义了默认值10.5。于是输入模型会按默认值补全tax = 10.5,替换后bartax就从 20.2 变成了 10.5。同理,tags也会被重置为空列表[]

这就是教程特别强调的警告:PUT会以"新请求体 + 模型默认值"作为完整的最终状态,凡是请求体没带的字段,都会被默认值覆盖。如果这不是你想要的行为,就应该改用PATCH做部分更新。

PATCH做部分更新

部分更新的核心思路

部分更新的目标是:只更新用户在请求里明确发送的字段,其余已存储的字段原样保留。核心代码见 docs_src/body_updates/tutorial002_py310.py:

@app.patch("/items/{item_id}") async def update_item(item_id: str, item: Item) -> Item: stored_item_data = items[item_id] stored_item_model = Item(**stored_item_data) update_data = item.model_dump(exclude_unset=True) updated_item = stored_item_model.model_copy(update=update_data) items[item_id] = jsonable_encoder(updated_item) return updated_item

逐行拆解(对应教程中的 7 步流程):

  1. 取出已存储的数据stored_item_data = items[item_id]
  2. 把已存数据放进 Pydantic 模型stored_item_model = Item(**stored_item_data),这样后续所有字段操作都统一在模型层进行;
  3. 从输入模型生成"只含用户显式设置字段"的 dictupdate_data = item.model_dump(exclude_unset=True)
  4. 基于已存模型做副本并应用增量更新updated_item = stored_item_model.model_copy(update=update_data)
  5. 把更新后的模型转成可存储形式jsonable_encoder(updated_item),写回存储;
  6. 返回更新后的模型(FastAPI 会自动做 JSON 兼容编码与response_model校验)。

Pydantic 的exclude_unset:只保留"用户真正设置过"的字段

item.model_dump(exclude_unset=True)是整个部分更新技巧的基石(对应教程小节 "Using Pydantic'sexclude_unsetparameter")。

  • 不带参数时,model_dump()会输出所有字段,包括那些没有被设置、只走了默认值的字段(例如未发送的tax会被输出为10.5);
  • 加上exclude_unset=True后,输出仅包含创建模型时实际被赋值(set)的字段,默认值字段被剔除。

于是,当客户端只发送{"name": "Barz"}时,update_data就只是{"name": "Barz"},而不是{"name": "Barz", "description": None, "price": None, "tax": 10.5, "tags": []}。这一点是"不覆盖已存值"的关键:如果不过滤默认值,stored_item_model中已存储的tax: 20.2就会被update_data里的默认值10.5覆盖,重蹈PUT的覆辙。

Pydantic 的update参数:在副本上应用增量

stored_item_model.model_copy(update=update_data)(对应教程小节 "Using Pydantic'supdateparameter")做的事情是:

  • 复制一份已存储的模型(stored_item_model本身不会被修改);
  • update_data这个 dict 里的键值覆盖副本上对应的属性
  • 返回新副本updated_item

由于update_data只包含用户显式发送的字段,副本上未被触及的descriptionpricetaxtags都会保留原有的存储值——这就实现了真正意义上的"部分更新"。

为什么最后还要用jsonable_encoder

教程的步骤 7 专门解释了这一点:把复制后的模型转成可存储形式(例如用jsonable_encoder)。这和使用.model_dump()再次导出相似,但jsonable_encoder额外保证了值一定是 JSON 可转换的数据类型——例如datetime会被转成str。这与 PUT 示例中对jsonable_encoder的使用理由完全一致:存储层(尤其是 NoSQL 数据库)通常只认 JSON 原生类型。

部分更新效果验证:仓库测试用例

仓库为 PATCH 示例提供了完整的测试:tests/test_tutorial/test_body_updates/test_tutorial002.py。其中test_patch_name这个用例非常直观地验证了"只更新发送的字段"这一行为:

def test_patch_name(client: TestClient): response = client.patch( "/items/bar", json={"name": "Barz"}, ) assert response.json() == { "name": "Barz", "description": "The bartenders", "price": 62, "tax": 20.2, "tags": [], }

注意这个断言:请求只发了name,但返回的tax依然是原先存储的20.2(而不是模型默认值10.5),descriptionprice也原样保留——这正是exclude_unset + model_copy(update=...)组合拳的效果。测试中还包含test_patch_all(发送全部字段时全量更新)和test_openapi_schema(校验 PATCH 端点在 OpenAPI 中的请求体/响应体定义),PUT示例的对应测试位于 tests/test_tutorial/test_body_updates/test_tutorial001.py。

部分更新流程总结

把上面的分析归纳成教程给出的完整流程清单,实现部分更新需要依次完成:

  1. (可选)使用PATCH而非PUT
  2. 取出已存储的数据;
  3. 将已存数据放入 Pydantic 模型;
  4. exclude_unset从输入模型生成不含默认值的 dict;
    • 这样只更新用户实际设置的值,不会用模型默认值覆盖已存储的值;
  5. model_copy(update=...)复制已存模型并应用收到的部分更新;
  6. jsonable_encoder把复制后的模型转成可存储的数据(类似再次.model_dump(),但确保datetimestr等 JSON 兼容转换);
  7. 保存数据到数据库;
  8. 返回更新后的模型。

技巧:同一套技巧完全可以用在PUT操作上——本教程示例使用PATCH只是因为它是为这种部分更新的使用场景而设计的。

一个重要前提:输入模型仍是全量校验

使用部分更新时不要忘记:FastAPI 依然会对输入请求体做完整的 Pydantic 校验(测试用例中的 422 响应定义也印证了这一点)。因此,如果你希望客户端可以省略任意字段,就必须让输入模型的所有字段都是 optional 的——要么带默认值,要么默认值为None

这带来一个建模上的取舍:用于创建的模型通常要求必填字段,而用于更新的模型要求全 optional。要区分这两类模型,可以参考教程中「额外模型(Extra Models)」一节的思路——为更新单独定义一个"所有字段皆可选"的模型,而不是让创建模型与更新模型混用。上面两个示例中的Item模型恰好把所有字段都定义为可选(namedescriptionprice默认Nonetax默认10.5tags默认[]),因此既能用于创建也能用于部分更新,但从 OpenAPI 生成的角度看,为两种语义分别建模通常更清晰。

小结

  • PUT= 整体替换:请求体缺失的字段会被模型默认值补全并覆盖已存值,适合"客户端提交完整资源"的场景;
  • PATCH= 部分更新:通过model_dump(exclude_unset=True)提取用户显式设置的字段,再用model_copy(update=...)应用到已存模型上,最后经jsonable_encoder转存,实现"只改发送的字段";
  • jsonable_encoder是连接模型与 JSON 存储层的桥梁,其类型转换规则可直接查阅 fastapi/encoders.py;
  • 两套写法的可运行示例与测试分别位于 docs_src/body_updates/ 与 tests/test_tutorial/test_body_updates/,可作为实战模板直接参考。

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询