FastAPI 请求体(Body)多参数实战:Path/Query/Body 混用与 embed 嵌入全解析
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
FastAPI 的核心魅力之一,是让你在path operation function中用 Python 类型标注自然地声明请求参数。本文聚焦官方教程中“请求体多参数”这一进阶主题,系统讲解如何在一个 HTTP 请求里同时接收多个 body 对象、让标量值以Body形式进入请求体、以及用embed让单个模型也拥有“键包裹”结构。全文以官方西班牙语文档 body-multiple-params(英文版见 body-multiple-params.md)为骨架,并结合仓库内docs_src/body_multiple_params/下的可直接运行示例与 FastAPI 源码实现展开,读完你便能自如设计出多模型、多来源混用的更新类接口。
预备知识:本篇涉及的核心声明工具
在进入正题前,先明确本教程反复出现的三类“取值来源”声明方式:
Path(...):从 URL 路径中取值,例如/items/{item_id}中的item_id;Query(...):从 URL 查询字符串中取值,例如?q=foo中的q;Body(...)与 Pydantic 模型参数:从请求体 JSON 中取值。
关于Path与Query的完整参数细节属于前置章节内容;本文直接从“混用”开始,并深入讲解 FastAPI 对请求体参数的特殊调度逻辑。所有示例代码均可在仓库 docs_src/body_multiple_params/ 目录中找到对应可运行文件。
混用Path、Query与 body 参数
FastAPI 允许你在同一个path operation function中自由混用三类参数声明,它会依据参数“形态”自动判断取值位置:路径中的变量、查询串里的标量、模型对象则从请求体读取。
看官方第一个示例 tutorial001_an_py310.py:
from typing import Annotated from fastapi import FastAPI, Path from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None @app.put("/items/{item_id}") async def update_item( item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], q: str | None = None, item: Item | None = None, ): results = {"item_id": item_id} if q: results.update({"q": q}) if item: results.update({"item": item}) return results在这个接口中:
item_id声明为Annotated[int, Path(...)],带上了标题与数值范围校验(ge=0, le=1000),从/items/{item_id}路径中解析;q: str | None = None是普通标量且默认值为None,FastAPI 自动把它当作可选的查询参数;item: Item | None = None是 Pydantic 模型,默认值同样是None,因此它作为 body 参数也是可选的。
注意:
q之所以被识别为查询参数,是因为声明函数参数时必须位于没有默认值的参数之前,或者借助*/Annotated排列位置;而这里将item的默认值设为None表示“请求中可以不携带该 body”。当它缺失时,函数内直接拿到None,所以代码里用if item:做了保护性判断。
这一写法把“取哪个来源”的决定权完全交给 FastAPI 的类型推断,开发者只需关注参数本身。底层上,这些参数的取值位置是在依赖解析阶段被判定并分组(path 组、query 组、body 组)的,相关逻辑分布在 fastapi/dependencies/utils.py 与 fastapi/routing.py 的依赖构建过程中。
同时声明多个请求体参数
上面的示例只有一个 body 参数item,此时请求体就是该模型的字段本身。而一旦你在函数里声明两个或更多Pydantic 模型 body 参数,行为会发生质变:FastAPI 会把每个参数名作为请求体的一个顶层键来组织数据。
官方示例 tutorial002_py310.py 演示了item与user两个模型并存:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None class User(BaseModel): username: str full_name: str | None = None @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, user: User): results = {"item_id": item_id, "item": item, "user": user} return results此时,客户端必须提交如下“带键包裹”的 JSON,而不是直接把字段摊平:
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }, "user": { "username": "dave", "full_name": "Dave Grohl" } }注意:与只有一个 body 参数时不同,
item现在必须位于请求体顶层的"item"键之下,同时user同理放在"user"键下。这一点容易踩坑——多模型并存会“自动启用”键包裹模式。
FastAPI 会把整个请求体按模型分别反序列化:item参数收到Item实例、user参数收到User实例,随后对每个模型执行 Pydantic 的复合校验(类型转换、必填字段检查、嵌套验证等),并且把拆分后的 schema 准确反映到 OpenAPI 文档与自动生成的可交互 API 文档中。
这个“自动键包裹”背后有一条确切的源码规则。在 fastapi/dependencies/utils.py 的_should_embed_body_fields()函数(约第 888 行)中可以看到判定逻辑:
# 按名字去重后,若存在多于一个不同的 body 字段 => 必须嵌入(embed) if len(body_param_names_set) > 1: return True也就是说:只要不同名的 body 字段数大于 1,FastAPI 就必然以“参数名作为键”的方式解析请求体,与开发者是否书写Body无关。仓库还为此提供了对应测试,见 tests/test_tutorial/test_body_multiple_params/test_tutorial002.py,其中即断言请求体必须携带"item"与"user"两个键。
把标量单值放进请求体:使用Body
路径参数用Path、查询参数用Query,请求体中的标量值自然也有等价物——Body。它解决一个非常实际的问题:当你除了多个模型还想塞一个简单的数值/字符串键进同一个请求体时,该如何声明。
延续上面的场景,假设你想在item、user之外,再加入一个importance整数键。如果直接写成:
importance: int = 5由于它只是一个标量且带默认值,FastAPI 会默认把它当成查询参数,而不是请求体字段。要让 FastAPI 明确“请从请求体取值”,必须用Body()显式标注,见官方示例 tutorial003_an_py310.py:
from typing import Annotated from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None class User(BaseModel): username: str full_name: str | None = None @app.put("/items/{item_id}") async def update_item( item_id: int, item: Item, user: User, importance: Annotated[int, Body()] ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} return results此时 FastAPI 期待如下的请求体:
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }, "user": { "username": "dave", "full_name": "Dave Grohl" }, "importance": 5 }对importance而言,同样会经历类型转换(字符串"5"会被转换为整数5)、校验、文档化等一系列流程。
Body函数的完整参数面
Body并不只是“标记来源”的空壳。查看其实现 fastapi/param_functions.py 中从约第 1323 行开始的Body()函数定义,以及承载它的Body参数类 fastapi/params.py(约第 469 行class Body(FieldInfo)),可以看到它与Query、Path共享同一套丰富的校验与元数据参数面,其中包括:
default/default_factory:默认值或默认值工厂,用于让该字段可选;embed: bool | None:控制是否用参数名作为键包裹(详见下文第五节);media_type:OpenAPI 中该字段的媒体类型描述,默认application/json;alias、title、description、examples:文档与序列化相关元数据;gt/ge/lt/le:数值大小校验;min_length/max_length/pattern:字符串长度与正则校验;multiple_of、max_digits、decimal_places、strict、allow_inf_nan:更细粒度的数值策略;include_in_schema、deprecated、json_schema_extra:控制 schema 呈现。
凡是Query、Path能做的参数级校验,Body也都能做。这也是官方文档在第五节特别强调“Body同样具备Query、Path等的全部附加校验与元数据参数”的原因。
请求体多参数再叠加查询参数
请求体的键布局与查询字符串彼此独立,因此你完全可以在拥有多个 body 参数的同时继续追加 query 参数。由于默认情况下“标量”都被解读为查询参数,这种裸标量你无需再用Query()包裹。
官方示例 tutorial004_an_py310.py 综合展示了全部三种来源同屏共存:
from typing import Annotated from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None class User(BaseModel): username: str full_name: str | None = None @app.put("/items/{item_id}") async def update_item( *, item_id: int, item: Item, user: User, importance: Annotated[int, Body(gt=0)], q: str | None = None, ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: results.update({"q": q}) return results需要注意的点:
- 函数签名开头使用了
*,把所有参数强制为仅限关键字参数,从而避免 Python 对“无默认值参数必须位于有默认值参数之前”的语法限制——这是同时存在必填与可选参数的常用写法; item_id是路径参数,importance: Annotated[int, Body(gt=0)]是带“必须大于 0”校验的 body 标量;q: str | None = None无需任何包装,自动成为可选查询参数,因此调用 URL 形如:
PUT /items/1?q=somequery请求体结构则与前一小节完全相同(item、user、importance三个顶层键)。官方对Body(gt=0)等校验的使用同样有测试覆盖,见 tests/test_tutorial/test_body_multiple_params/test_tutorial004.py。
单个模型参数使用embed=True强制键包裹
假如接口里只有一个Pydantic 模型参数item,默认情况下 FastAPI 期望请求体就是模型内容本身:
{ "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }但你可能出于以下原因希望它也和“多模型模式”一样,被包裹在"item"键之下:
- 与其它同类接口保持一致的键布局(例如所有更新类接口都形如
{"item": {...}}); - 未来要向后兼容地追加其它顶层键;
- 客户端 SDK 生成或前端代码统一样式。
此时,使用Body的专用参数embed即可,写法如下:
item: Annotated[Item, Body(embed=True)]官方示例 tutorial005_an_py310.py 展示了完整实现:
from typing import Annotated from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None @app.put("/items/{item_id}") async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): results = {"item_id": item_id, "item": item} return results设置embed=True后,FastAPI 期望的请求体从“摊平”变成“键包裹”:
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 } }而不是:
{ "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }源码中的 embed 判定逻辑
embed之所以是可选参数,是因为它背后存在一套自动化的判定规则。回到前文提到的 fastapi/dependencies/utils.py 的_should_embed_body_fields(),其完整决策链大致是:
- 若没有任何 body 字段,返回
False(无需嵌入); - 将 body 字段按名字去重后,若多于一个不同名字,返回
True(多参数自动键包裹,对应本文第三节); - 若某字段的
field_info.embed被显式设置,则按其取值返回(对应本节Body(embed=True)); - 对
Form/File类字段还有额外规则,以保证能从表单数据中提取键值对。
而embed标志在 fastapi/routing.py 中通过embed_body_fields等属性在路由与请求体字段模型(如BodyModelField相关的请求体字段定义路径)之间传递,最终决定解析后的单值/多值 body 字段是否以键形式读取。这一机制同时被两个官方测试覆盖:tests/test_tutorial/test_body_multiple_params/test_tutorial001.py(单模型不包裹)与 tests/test_tutorial/test_body_multiple_params/test_tutorial005.py(embed=True后必须携带"item"键)。
embed默认值由Body()参数embed: bool | None = None决定,语义是“自动判断”;Body(embed=True)则把判断结果强制锁定为嵌入。
小结
尽管 HTTP 协议层面一个请求只允许携带一个请求体,FastAPI 却允许你在函数签名里声明任意多个请求体参数,并把这份“别扭”转化为开发者的便利。回顾本教程核心要点:
- 三类来源可自由混用:路径、查询、请求体参数可以在同一个函数中并存,FastAPI 依据参数形态与标注自动分流;
- 多模型自动键包裹:同时声明
item、user等多个 Pydantic 模型参数时,FastAPI 自动以参数名为键、期望形如{"item": {...}, "user": {...}}的请求体,并由_should_embed_body_fields()保证判定一致性; - 标量也能进请求体:裸标量默认是查询参数,用
Body()显式标注后即可作为请求体中的顶层键,且能复用gt、min_length、alias等全套校验/元数据参数; - 查询参数不受影响:body 键布局复杂化不影响 URL 上的
?q=...查询串,二者互不干扰; embed=True控制包裹与否:单一模型默认“摊平”,需要与多模型一致的键包裹结构时,通过Body(embed=True)显式指定,也可留空让 FastAPI 按参数个数自动决策。
请求体解析完成后,FastAPI 会依次完成类型转换、复合数据校验,并在 OpenAPI schema 与自动文档中呈现正确结构——这意味文档里展示的交互式 Swagger UI 会自动生成带键的请求体示例,前后端联调时直接复制即可使用。
如需继续深入,建议按官方教程顺序依次阅读请求体字段约束(Field)、嵌套模型等后续章节;本文全部示例代码与测试均可在仓库 docs_src/body_multiple_params/ 与 tests/test_tutorial/test_body_multiple_params/ 中直接查阅、运行验证。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考