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.
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:
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.The default
[]keepstagsoptional. 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:
set[str]is a set of unique strings. Send["python", "python", "fastapi"]and you get back just{"python", "fastapi"}.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:
authoris typed asAuthor, another model. FastAPI now expects a nested JSON object there.Each layer is validated on its own. A bad
usernameinsideauthorgets caught just like a badtitleon 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] = NoneLet's review:
HttpUrlchecks the value is a valid URL. Send"not a url"and FastAPI rejects it before your code runs.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:
commentsislist[Comment], so the JSON carries an array of comment objects.Every comment in that array is validated against the
Commentmodel. 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.
