FastAPI Interactive Docs

Body - Nested Models

In the last post, Body - Fields, we put rules on individual fields inside our Blog model. But every field so far has been flat: a string, a number, a bool. Real data is rarely that tidy. A blog has a list of tags, and it has an author who is an object in their own right. 🤔

Pydantic handles all of that. You can nest lists, sets, and even whole models inside other models, and FastAPI will validate every layer for you. Let's build our Blog up piece by piece.

List fields

Say a blog carries tags. The simplest version is a plain list.

main.py
from typing import Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel):    title: str    content: str    tags: list = []@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Blog):    return {"blog_id": blog_id, "blog": blog}

This makes tags a list, but it says nothing about what is inside it. Numbers, strings, anything goes. We can do better.

Typing the list

Add a type parameter in square brackets to say what the list holds.

class Blog(BaseModel):    title: str    content: str    tags: list[str] = []

Let's review:

  1. list[str] means "a list of strings." Send a tag that is a number and FastAPI will try to coerce it, then complain if it cannot.

  2. The default [] keeps tags optional. Leave it out and you get an empty list.

Sets for unique items

Tags probably should not repeat. Python's set is built for unique items, and Pydantic supports it directly.

class Blog(BaseModel):    title: str    content: str    tags: set[str] = set()

Let's review:

  1. set[str] is a set of unique strings. Send ["python", "python", "fastapi"] and you get back just {"python", "fastapi"}.

  2. Deduplication happens automatically, both on the way in and on the way out. No manual cleanup.

Nesting a whole model

Here is the real upgrade. A field's type can be another Pydantic model. Let's give our Blog an Author.

python

from typing import Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Author(BaseModel):    username: str    full_name: Optional[str] = Noneclass Blog(BaseModel):    title: str    content: str    tags: set[str] = set()    author: Optional[Author] = None@app.put("/blog/{blog_id}")async def update_blog(blog_id: int, blog: Blog):    return {"blog_id": blog_id, "blog": blog}

Let's review:

  1. author is typed as Author, another model. FastAPI now expects a nested JSON object there.

  2. Each layer is validated on its own. A bad username inside author gets caught just like a bad title on the blog.

FastAPI now expects a body like this:

json

{    "title": "My First Blog",    "content": "Hello FastAPI!",    "tags": ["python", "fastapi"],    "author": {        "username": "dave",        "full_name": "Dave Grohl"    }}

Special types like HttpUrl

Pydantic ships types that validate more than a plain str. For example, a profile link should be a real URL, so use HttpUrl.

from pydantic import BaseModel, HttpUrlclass Author(BaseModel):    username: str    full_name: Optional[str] = None    website: Optional[HttpUrl] = None

Let's review:

  1. HttpUrl checks the value is a valid URL. Send "not a url" and FastAPI rejects it before your code runs.

  2. It also shows up in the auto-generated docs as a URL, so clients know what to send.

Lists of submodels

A field can be a list of models too. Say each blog has a list of comments.

class Comment(BaseModel):    user: str    text: strclass Blog(BaseModel):    title: str    content: str    tags: set[str] = set()    author: Optional[Author] = None    comments: list[Comment] = []

Let's review:

  1. comments is list[Comment], so the JSON carries an array of comment objects.

  2. Every comment in that array is validated against the Comment model. One bad entry and the whole request is rejected.

This is how you reach arbitrarily nested data: models inside lists inside models, as deep as you need, all validated automatically.

⏭️Up next: Declare Request Example Data. Our models are getting rich, and the auto-generated docs would be friendlier with sample values filled in. Next we will show FastAPI how to display example data so your /docs page is easy for anyone to try.

Practice Lab: