FastAPI Interactive Docs

Body - Multiple Parameters

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.

main.py
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 result

Let's review:

  1. blog_id lives in the path, so FastAPI reads it from the URL. The Path(ge=0) keeps it from going negative, just like in the numeric validations post.

  2. q is a plain Optional[str], so it stays a query parameter.

  3. blog is a Pydantic model with a default of None, 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.

main.py
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:

  1. There are now two models in the signature, blog and author. Since both are Pydantic models, FastAPI knows neither one is a query parameter.

  2. Because there is more than one body parameter, FastAPI uses the parameter names as keys in the JSON.

  3. blog_id is 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:

  1. Body() is the body twin of Query and Path. It tells FastAPI to look for importance inside the JSON body, not the URL.

  2. importance now becomes another top-level key in the body, sitting next to blog and author.

  3. Body accepts the same validation extras as Query and Path, so Body(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:

  1. There is only one body parameter, yet embed=True makes FastAPI expect the model nested under a blog key.

  2. Without embed, the body would be the Blog fields 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. 😁

Practice Lab: