Pydantic Models
Table of Contents
Why Pydantic Matters in FastAPI
FastAPI is built around data validation and data serialization. Pydantic is the library that powers this. Every time you:
- Receive JSON data in a request body, or
- Return a JSON response with a defined structure,
FastAPI uses Pydantic models to convert raw data into Python objects and validate that the data is correct.
Pydantic models are like "smart dictionaries" with:
- Defined fields and types
- Automatic type conversion
- Automatic validation
- Helpful error messages
In this chapter, you will see how to use Pydantic models in FastAPI for both input and output.
Basic Pydantic Model
A Pydantic model is a Python class that inherits from BaseModel and uses type hints.
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
is_active: boolThis defines a data shape:
| Field | Type | Description |
|---|---|---|
id | int | Numeric identifier |
name | str | User's name |
email | str | User's email |
is_active | bool | Whether the user is active |
Pydantic will:
- Create
Userinstances:User(id=1, name="Alice", email="a@example.com", is_active=True) - Validate data: wrong types will raise errors
- Convert types when possible, for example
"1"to1
Key rule: Every field in a Pydantic model must have a type hint. Pydantic uses type hints to validate and convert data.
Using Models in FastAPI Requests
Pydantic models are most visible in request bodies. You declare them as function parameters.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
in_stock: bool = True
@app.post("/items/")
async def create_item(item: Item):
# item is a validated Pydantic model here
return {"message": "Item received", "item": item}
What happens when a client sends JSON to /items/:
- FastAPI reads the body and parses JSON.
- It uses the
Itemmodel to validate the data. - It creates an
Iteminstance and passes it as theitemparameter. - If validation fails, FastAPI returns a 422 error with details.
Example request
Request body:
{
"name": "Laptop",
"description": "A powerful laptop",
"price": 1299.99,
"in_stock": false
}
Inside the endpoint, item is an Item instance:
item.name # "Laptop"
item.price # 1299.99
item.in_stock # False
item.description # "A powerful laptop"If the client sends:
{
"name": "Laptop",
"price": "1299.99"
}Pydantic will:
- Convert
"1299.99"to1299.99(float) - Use the default
in_stock=True - Use the default
description=None
Default Values and Optional Fields
You can control which fields are required and which are optional.
Required fields
Fields without a default value are required.
class Product(BaseModel):
name: str # required
price: float # required
in_stock: bool # requiredAll three must be provided.
Optional fields with default values
Fields with a default value are optional.
class Product(BaseModel):
name: str
price: float
in_stock: bool = True # optional, defaults to True
Here, the client can omit in_stock. It becomes True.
Optional type vs optional value
from typing import Optional
class UserProfile(BaseModel):
username: str
bio: Optional[str] = None
age: int | None = Nonebiocan bestrorNone.agecan beintorNone.- Both are optional, since they have defaults.
If you want a field that must be present but can be null in JSON:
from pydantic import BaseModel
class Example(BaseModel):
value: int | None # required, but can be null
The client must send "value": null or a number. Omitting value causes a validation error.
Field Types and Validation
Pydantic supports many types and performs validation based on them.
Some common types:
| Type | Example | Validation example |
|---|---|---|
int | age: int | Rejects non integer values |
float | price: float | Converts "10.5" to 10.5 |
bool | is_active: bool | Converts "true" to True |
str | name: str | Converts numbers to strings if possible |
datetime | created_at: datetime | Parses ISO datetime strings |
date | birthday: date | Parses "2020-01-01" |
list[int] | tags: list[int] | Ensures a list, with each element an int |
dict[str, int] | scores: dict[str, int] | Keys are strings, values are ints |
EmailStr | email: EmailStr | Validates email format |
UUID | id: UUID | Validates UUID string |
HttpUrl | url: HttpUrl | Validates URL format |
You can import some special types from pydantic:
from pydantic import BaseModel, EmailStr, HttpUrl
class Contact(BaseModel):
email: EmailStr
website: HttpUrl | None = NoneIf the client sends an invalid email, Pydantic returns an error.
Nested Models
You can nest Pydantic models inside each other. This is very common in real APIs.
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
country: str
class User(BaseModel):
id: int
name: str
address: Address
Example JSON for a User:
{
"id": 1,
"name": "Alice",
"address": {
"street": "Main Street 1",
"city": "Springfield",
"country": "USA"
}
}Pydantic will:
- Create an
Addressinstance for theaddressfield. - Create a
Userinstance that contains theAddress.
In your code:
user.address.city # "Springfield"You can also have lists of nested models:
class OrderItem(BaseModel):
product_id: int
quantity: int
class Order(BaseModel):
id: int
items: list[OrderItem]Request body example:
{
"id": 123,
"items": [
{"product_id": 1, "quantity": 2},
{"product_id": 2, "quantity": 1}
]
}Models for Input vs Output
You do not need to use the same model for input and output. Often, you should not.
Examples:
- Input model for creating a user: no
id, nocreated_at, nohashed_password. - Output model: includes
id,created_at, but not thepassword.
from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
email: str
password: str
class UserRead(BaseModel):
id: int
name: str
email: str
is_active: boolYou can use them in FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.post("/users/", response_model=UserRead)
async def create_user(user_in: UserCreate):
# Example: pretend this user was saved to a database
db_user = {
"id": 1,
"name": user_in.name,
"email": user_in.email,
"is_active": True,
"hashed_password": "not-shown"
}
# FastAPI will convert this dict into a UserRead
return db_userImportant points:
user_inis aUserCreatemodel to validate input.response_model=UserReadcontrols the response shape.- Even if your function returns extra fields (like
hashed_password), FastAPI will remove them when converting toUserRead.
Important rule: Use separate models for input and output when you need to hide sensitive data, such as passwords or internal fields.
Model Configuration and Aliases
Sometimes JSON field names do not match Python variable names. For example, the client sends user_id but you prefer userId in Python, or you must follow an existing external API.
You can define field aliases using Field.
from pydantic import BaseModel, Field
class Item(BaseModel):
item_id: int = Field(alias="itemId")
price_in_cents: int = Field(alias="priceInCents")Now Pydantic will:
- Accept JSON like:
{
"itemId": 10,
"priceInCents": 999
}- Create an
Itemobject with attributesitem_id=10,price_in_cents=999.
By default, when converting the model back to JSON, Pydantic uses the internal field names, unless you specify by_alias=True:
item = Item(itemId=10, priceInCents=999)
item.dict() # {'item_id': 10, 'price_in_cents': 999}
item.dict(by_alias=True) # {'itemId': 10, 'priceInCents': 999}
In FastAPI, response_model uses by_alias=True by default, so clients see aliases.
You can control validation behavior with Config (Pydantic v1) or model_config (Pydantic v2). Since FastAPI is moving to Pydantic v2, here is v2 style:
from pydantic import BaseModel, ConfigDict
class Item(BaseModel):
model_config = ConfigDict(
populate_by_name=True, # allow using internal field names too
extra="forbid" # reject unknown fields
)
item_id: int = Field(alias="itemId")Now:
- The client can send
itemIdoritem_id. - Any extra fields not defined in the model will cause a validation error.
Converting Models to Dicts and JSON
Pydantic models are normal Python objects, but they have helpful methods.
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
description: str | None = None
item = Item(name="Book", price=9.99)You can convert to a dictionary:
data = item.model_dump()
# {'name': 'Book', 'price': 9.99, 'description': None}Common options:
item.model_dump(exclude_none=True)
# {'name': 'Book', 'price': 9.99}
item.model_dump(include={"name", "price"})
# {'name': 'Book', 'price': 9.99}
item.model_dump(exclude={"description"})
# {'name': 'Book', 'price': 9.99}You can also get a JSON string:
json_str = item.model_dump_json()
# '{"name": "Book", "price": 9.99, "description": null}'
In FastAPI endpoints, you rarely need to call these manually, because FastAPI converts return values into JSON automatically, using the response_model when defined.
Validation Errors and Error Responses
When data does not match the Pydantic model, FastAPI returns a 422 response with error details.
Example model:
class Item(BaseModel):
name: str
price: floatClient sends:
{
"name": 123,
"price": "abc"
}Pydantic will:
- Convert
name: 123to"123"successfully. - Fail to convert
price: "abc"to float.
The client gets a response like:
{
"detail": [
{
"type": "float_parsing",
"loc": ["body", "price"],
"msg": "Input should be a valid number",
"input": "abc"
}
]
}This strongly typed validation is one of the main reasons FastAPI and Pydantic work so well together.
Example: Simple CRUD Shapes
Imagine you are building a basic API for managing tasks. You might define several Pydantic models.
from pydantic import BaseModel
from datetime import datetime
class TaskBase(BaseModel):
title: str
description: str | None = None
class TaskCreate(TaskBase):
# same fields as TaskBase, no extra fields
pass
class TaskUpdate(BaseModel):
title: str | None = None
description: str | None = None
completed: bool | None = None
class TaskRead(TaskBase):
id: int
completed: bool
created_at: datetimeUse them in FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.post("/tasks/", response_model=TaskRead)
async def create_task(task_in: TaskCreate):
# fake database object
db_task = {
"id": 1,
"title": task_in.title,
"description": task_in.description,
"completed": False,
"created_at": datetime.utcnow(),
}
return db_task
@app.patch("/tasks/{task_id}", response_model=TaskRead)
async def update_task(task_id: int, task_in: TaskUpdate):
# use only the fields that are not None
update_data = task_in.model_dump(exclude_unset=True)
# pretend we update the database with update_data
# and then return the updated task
updated_task = {
"id": task_id,
"title": update_data.get("title", "Old title"),
"description": update_data.get("description", "Old desc"),
"completed": update_data.get("completed", False),
"created_at": datetime.utcnow(),
}
return updated_taskImportant here:
TaskCreateis for creating tasks. All fields are required except those with defaults.TaskUpdateis for partial updates. All fields are optional and can be omitted.model_dump(exclude_unset=True)returns only the fields the client sent.
Summary
- Pydantic models are Python classes that define and validate data structures.
- FastAPI uses Pydantic models to:
- Validate request bodies.
- Serialize responses using
response_model. - Use type hints and default values to control required and optional fields.
- Use nested models to represent complex objects.
- Use different models for input and output to hide sensitive data or internal details.
- Use
Fieldfor aliases and extra field configuration. - Use methods like
model_dumpandmodel_dump_jsonto convert models to dicts or JSON when needed.
In the next chapters on FastAPI, these Pydantic models will become the foundation for request bodies, response models, and advanced validation.
Views: 7
KAHIBARO