FastAPI 多 Body 参数详解:混合 Path/Query/Body、Body() 函数与 embed 嵌入机制
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
本篇基于 FastAPI 官方教程文档Body – Mehrere Parameter(docs/de/docs/tutorial/body-multiple-params.md)展开,讲解当请求体不再只有一个 Pydantic 模型时的进阶用法:如何自由混合Path、Query与 Body 参数、如何声明多个 Body 参数、如何用Body把单个值收进请求体、以及用embed=True让单个 Body 参数也按"键包裹"的方式接收。读完本文,你可以掌握组合式请求体的完整声明方式,并从源码层面理解 FastAPI 判断"何时自动嵌入"的底层逻辑。
混用 Path、Query 与 Body 参数
在已经掌握Path与Query的基础上,FastAPI 允许你在同一个路径操作函数中自由混合Path、Query与请求体(Body)参数声明,框架会自动判断每个参数的数据来源。并且 Body 参数也可以设为可选——只需把默认值设为None:
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对应仓库示例文件:tutorial001_an_py310.py。
要点说明:
item_id: Annotated[int, Path(...)]从 URL 路径取值,并可附加ge=0, le=1000之类的数值约束与title元数据;q: str | None = None是单个值参数,FastAPI 默认将其解释为Query 参数(即?q=...),不要求显式写Query(...);item: Item | None = None是 Pydantic 模型参数,FastAPI 将其识别为Body 参数。注意此处的关键细节:由于Item | None的默认值是None,这个 Body 参数是可选的——请求可以不带请求体。
注意:在这种情况下,来自 Body 的
item是可选的,因为它以None作为默认值。
多个 Body 参数:自动以参数名为键嵌入
在上述示例中,路径操作期望一个包含Item各字段的 JSON 请求体,例如:
{ "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }但当你需要同时接收多个模型(例如"物品"和"操作用户")时,可以声明多个 Body 参数:
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对应仓库示例文件:tutorial002_py310.py。
此时 FastAPI 会检测到函数中不止一个 Body 参数(两个 Pydantic 模型参数),于是**以参数名作为键(字段名)**来组织请求体,期望的 Body 变为:
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }, "user": { "username": "dave", "full_name": "Dave Grohl" } }注意:尽管
item的声明方式与之前相同,但现在它被期望位于 Body 中item这个键之下。
FastAPI 会自动完成请求数据的转换——item参数得到它专属的嵌套内容,user参数同理;并对这份组合数据进行校验,同时将其记录进 OpenAPI Schema 与自动生成的文档中。
源码与测试佐证:组合 Schema 的生成与校验位置
从源码结构看,"多个 Body 参数自动嵌入"的判定发生在依赖解析阶段。fastapi/dependencies/utils.py 中的_should_embed_body_fields()函数给出了明确规则:
def _should_embed_body_fields(fields: list[ModelField]) -> bool: if not fields: return False # More than one dependency could have the same field, it would show up as multiple # fields but it's the same one, so count them by name body_param_names_set = {field.name for field in fields} # A top level field has to be a single field, not multiple if len(body_param_names_set) > 1: return True first_field = fields[0] # If it explicitly specifies it is embedded, it has to be embedded if getattr(first_field.field_info, "embed", None): return True ...规则可以归纳为:
- Body 参数名超过一个→ 必须嵌入(自动按键包裹);
- 任一参数显式设置了
embed=True→ 必须嵌入; Form/File字段若不是BaseModel(或其联合),也需嵌入以便提取键值对。
真正的取值与校验在 request_body_to_args() 中完成:当single_not_embedded_field(只有一个 Body 字段且未嵌入)成立时,整个请求体直接作为该字段的值进行校验;否则逐个字段执行body_to_process.get(get_validation_alias(field)),即按键名从 Body 字典中提取,再对每个字段独立做 Pydantic 校验——这解释了为什么多 Body 参数时每个参数各自嵌套在顶层键下。
仓库测试 test_tutorial002.py 完整验证了上述行为:
test_post_all:发送{"item": {...}, "user": {...}}返回 200,缺失字段(description、full_name)自动补None;test_post_no_body/test_post_no_item/test_post_no_user:缺少任一键即返回 422,且loc精确到["body", "item"]或["body", "user"],便于定位错误来源;test_post_missing_required_field_in_item:嵌套字段缺失时,loc会细化到["body", "item", "price"];test_openapi_schema:快照断言 OpenAPI Schema 中会自动生成一个组合请求体 SchemaBody_update_item_items__item_id__put,其properties为item与user两个$ref,且required: ["item", "user"]——证明组合结构会被如实写入文档,前端可直接据此调用。
单个值收进 Body:Body()的用途
与用于 Query 和 Path 参数的Query、Path相对应,FastAPI 提供Body来为 Body 参数声明额外信息。例如在上面的双模型基础上,你还想在同一个 Body中多加一个importance键:
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对应仓库示例文件:tutorial003_an_py310.py。
如果直接写importance: int而不加Body(),FastAPI 会把它当作Query 参数(因为它是单个值)。使用Annotated[int, Body()]后,它被识别为 Body 中的另一个键,期望的请求体为:
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }, "user": { "username": "dave", "full_name": "Dave Grohl" }, "importance": 5 }数据类型转换、校验、文档生成等行为与模型参数一致。
多个 Body 参数与 Query 参数并存
Body 参数与 Query 参数可以任意组合。由于单个值参数默认就是 Query 参数,你甚至不需要显式写Query(...),直接q: str | None = None即可:
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对应仓库示例文件:tutorial004_an_py310.py。
这个例子还顺带展示了两个细节:
Body(gt=0):Body与Query、Path一样支持相同的附加校验与元数据参数。这里gt=0表示importance必须大于 0,非法值会触发 422 校验错误;- 参数列表开头的
*强制后续参数必须按关键字传入,避免位置参数误配——这在参数较多时是良好的防御性写法。
嵌入单个 Body 参数:embed=True
假设你只声明了一个itemBody 参数(类型为 Pydantic 模型Item)。默认情况下,FastAPI 期望请求体直接就是模型内容。但若你希望它像多参数场景那样,在item键下接收模型内容,可以设置Body的embed参数:
item: Annotated[Item, Body(embed=True)]完整示例:
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对应仓库示例文件:tutorial005_an_py310.py。
此时 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参数的官方定义见 fastapi/param_functions.py:当embed=True时,参数会被期望"作为 JSON Body 中的一个键,而不是 JSON Body 本身";并且文档注明,声明多于一个 Body 参数时该行为会自动发生。
测试佐证的典型误区
test_tutorial005.py 中有一个值得注意的用例test_post_like_not_embedded:当设置embed=True后,若客户端仍然发送"未嵌入"的扁平结构{"name": "Foo", "price": 50.5},请求会返回 422,错误位置为["body", "item"]——即 FastAPI 在item键下找不到值,整个item字段被判定为缺失。这提醒开发者:切换embed语义属于接口契约变更,客户端必须同步调整,否则会产生隐蔽的 422 错误。
小结
一个请求只能有一个 Body,但这并不妨碍你在路径操作函数中声明多个 Body 参数:
- 自由混用
Path、Query、Body 参数,FastAPI 依据类型(Pydantic 模型 vs 单个值)与显式声明自动区分来源; - 多个模型参数会自动以参数名为键嵌入请求体,FastAPI 负责转换、校验,并生成正确的组合 OpenAPI Schema(如
Body_update_item_items__item_id__put)写入文档; - 单个值参数可用
Body()收进请求体,并可携带gt、ge等与Query/Path一致的校验和元数据参数; - 单个模型参数默认"裸接收"整个 Body,用
Body(embed=True)可强制按键包裹接收,从源码看该判定集中在_should_embed_body_fields()与request_body_to_args()(fastapi/dependencies/utils.py)中实现。
以上示例文件均位于 docs_src/body_multiple_params/,配套测试位于 tests/test_tutorial/test_body_multiple_params/,可直接运行验证各场景的请求/响应与 OpenAPI Schema 行为。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考