Last time, in Query Parameter Models, we bundled a pile of loose query parameters into one tidy Pydantic model. That cleaned up the URL side of things. Now let's turn to the body side and ask a bigger question: what if one request needs to carry more than one chunk of structured data? 🤔
So far our Blog model has been the only thing riding in the body. But a real request often needs a Blog and the author who wrote it, maybe plus a small flag or two. Good news: FastAPI lets you declare several body parameters in one endpoint and sorts them out for you.
Mixing Path, Query, and body, with an optional body
Before we add more bodies, a quick reminder that you can freely mix a path param, a query param, and a body in one function. You can even make the body optional by giving it a default of None.
from typing import Annotated, Optionalfrom fastapi import FastAPI, Pathfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: bool@app.put("/blog/{blog_id}")async def update_blog( blog_id: Annotated[int, Path(title="The blog ID", ge=0)], q: Optional[str] = None, blog: Optional[Blog] = None,): result = {"blog_id": blog_id} if q: result.update({"q": q}) if blog: result.update({"blog": blog}) return resultLet's review:
blog_idlives in the path, so FastAPI reads it from the URL. ThePath(ge=0)keeps it from going negative, just like in the numeric validations post.qis a plainOptional[str], so it stays a query parameter.blogis a Pydantic model with a default ofNone, so it is an optional body. Skip it and FastAPI will not complain.
Two body parameters at once
Here is the new trick. Declare two Pydantic models and FastAPI treats both as body parameters.
from typing import Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: boolclass Author(BaseModel): username: str full_name: Optional[str] = None@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Blog, author: Author): return {"blog_id": blog_id, "blog": blog, "author": author}Let's review:
There are now two models in the signature,
blogandauthor. Since both are Pydantic models, FastAPI knows neither one is a query parameter.Because there is more than one body parameter, FastAPI uses the parameter names as keys in the JSON.
blog_idis still a path parameter. Nothing changed there.
So FastAPI now expects a body shaped like this:
{ "blog": { "title": "My First Blog", "content": "Hello FastAPI!", "is_active": true }, "author": { "username": "dave", "full_name": "Dave Grohl" }}Notice how blog is no longer at the top level. It got tucked under a blog key, with author sitting right beside it. FastAPI validates each model separately and documents both in the auto-generated docs.
A single value in the body
What if you also want a plain value in the body, say an importance score? Declare it as a normal int and FastAPI will assume it is a query parameter, because singular types default to query. To force it into the body, reach for Body.
from typing import Annotated, Optionalfrom fastapi import Body, FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: boolclass Author(BaseModel): username: str full_name: Optional[str] = None@app.put("/blog/{blog_id}")async def update_blog( blog_id: int, blog: Blog, author: Author, importance: Annotated[int, Body()],): return { "blog_id": blog_id, "blog": blog, "author": author, "importance": importance, }Let's review:
Body()is the body twin ofQueryandPath. It tells FastAPI to look forimportanceinside the JSON body, not the URL.importancenow becomes another top-level key in the body, sitting next toblogandauthor.Bodyaccepts the same validation extras asQueryandPath, soBody(gt=0)would reject anything that is not above zero.
The body FastAPI now expects:
{ "blog": { "title": "My First Blog", "content": "Hello FastAPI!", "is_active": true }, "author": { "username": "dave", "full_name": "Dave Grohl" }, "importance": 5}Embedding a single body parameter
One last case. Say you are back to a single blog body. By default FastAPI expects its fields at the top level of the JSON. But if you would rather wrap it under a blog key, like it does when there are multiple bodies, pass embed=True.
from typing import Annotatedfrom fastapi import Body, FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str is_active: bool@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Annotated[Blog, Body(embed=True)]): return {"blog_id": blog_id, "blog": blog}Let's review:
There is only one body parameter, yet
embed=Truemakes FastAPI expect the model nested under ablogkey.Without
embed, the body would be theBlogfields directly. With it, you get the wrapped shape below.
{ "blog": { "title": "My First Blog", "content": "Hello FastAPI!", "is_active": true }}This is handy when you want a consistent body shape across all your endpoints, whether they carry one model or several.
Gotcha: FastAPI decides body versus query by type, not by name. A Pydantic model goes to the body automatically. A singular type like int or str goes to the query unless you wrap it in Body(). So if your importance keeps showing up as a query parameter you did not want, that missing Body() is your culprit. 😁
