To receive structured data (typically JSON) in a POST, PUT or PATCH request, you declare a parameter with a Pydantic model type. FastAPI reads the body, validates it and gives you a typed object.
from pydantic import BaseModel, Field, EmailStr
class Item(BaseModel):
name: str = Field(min_length=1, max_length=50, examples=["Keyboard"])
price: float = Field(gt=0, description="Price in USD")
is_offer: bool | None = None
owner_email: EmailStr | None = None
@app.post("/items/")
def create_item(item: Item):
return {"item_name": item.name, "price_with_tax": item.price * 1.12}
Field() adds constraints and documentation metadata.EmailStr requires pip install "pydantic[email]" and validates the format automatically.@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item, q: str | None = None):
return {"item_id": item_id, **item.model_dump(), "q": q}
FastAPI routes each parameter to its source: item_id from the path, item from the JSON body, q from the query string.
class Tag(BaseModel):
name: str
class Article(BaseModel):
title: str
tags: list[Tag] = []
metadata_: dict[str, str] | None = None
@app.post("/articles/")
def create_article(article: Article):
return {"title": article.title, "first_tag": article.tags[0].name if article.tags else None}
Arbitrarily deep nesting works: lists of models, dicts, optional fields. Validation errors point to the exact nested path.
from fastapi import Body
@app.put("/items/{item_id}")
def update(item_id: int, item: Item, user: User):
return {"item_id": item_id, "item": item.model_dump(), "user": user.model_dump()}
Two models in one function means the JSON body must be {"item": {...}, "user": {...}} (FastAPI embeds them automatically when there is more than one body parameter). With a single model you can force embedding using item: Item = Body(embed=True).
A: model_dump() returns a Python dict; model_dump_json() returns a JSON string directly (faster, no intermediate dict). In Pydantic v1 these were called .dict() and .json().
A: Only if you declare a response_model. Without it, whatever the function returns is serialized as-is. With it, the output is filtered and validated against the model (see the next page).
A: By default they are ignored. You can change this with model_config = ConfigDict(extra="forbid") to reject unknown fields with a 422 error, or extra="allow" to keep them.
Pydantic models are the contract of your API: declare once and you get validation, conversion, nested structures, editor autocompletion and schema generation. This single mechanism replaces a lot of manual parsing code.