📦 Pydantic Models: Your Data’s Security Guard
Imagine you’re running a fancy restaurant. Before anyone enters, a bouncer checks their ID, dress code, and reservation. That bouncer is Pydantic—it checks every piece of data before it enters your FastAPI app!
🎯 What You’ll Learn
- Request Body Basics
- Creating Pydantic Models
- Pydantic Field Validation
- Pydantic Field Defaults
- Nested Pydantic Models
- List and Dict Fields
- Model Inheritance
- Request Example Data
📨 Request Body Basics
What’s a Request Body?
When someone sends data TO your app (like filling a form), that data travels in the request body. It’s like sending a letter inside an envelope.
graph TD A[📱 User fills form] --> B[📦 Data packed in body] B --> C[🚀 Sent to server] C --> D[🔍 FastAPI reads it]
Simple Example:
# User sends this JSON:
{
"name": "Pizza",
"price": 9.99
}
The request body carries this JSON to your server. But how do you tell FastAPI what shape this data should have? That’s where Pydantic comes in!
🏗️ Creating Pydantic Models
Your First Model
A Pydantic model is like a blueprint for your data. It says “my data should look exactly like THIS.”
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
That’s it! You just created a bouncer that checks:
- ✅
namemust be text (string) - ✅
pricemust be a number (float)
Using It in FastAPI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return {"message": f"Created {item.name}"}
What happens:
- User sends JSON data
- Pydantic checks it matches the
Itemshape - If wrong? → Error message sent back
- If correct? → Your code runs!
✅ Pydantic Field Validation
Adding Rules to Fields
The bouncer can do more than check types. It can check rules!
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: float = Field(gt=0) # gt = greater than
quantity: int = Field(ge=1, le=100) # ge/le = greater/less or equal
Common Validation Rules
| Rule | Meaning | Example |
|---|---|---|
gt |
Greater than | gt=0 (must be > 0) |
ge |
Greater or equal | ge=1 (must be ≥ 1) |
lt |
Less than | lt=100 |
le |
Less or equal | le=99 |
min_length |
Min string length | min_length=3 |
max_length |
Max string length | max_length=50 |
Example with all rules:
class Product(BaseModel):
name: str = Field(
min_length=2,
max_length=100
)
price: float = Field(gt=0, le=10000)
stock: int = Field(ge=0)
If someone sends {"name": "", "price": -5, "stock": -1}, Pydantic catches ALL the errors!
🎁 Pydantic Field Defaults
Making Fields Optional
Sometimes data is optional. Like toppings on pizza—nice to have, not required!
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str # Required!
price: float # Required!
description: Optional[str] = None # Optional
tax: float = 0.0 # Default value
Three ways to set defaults:
class Order(BaseModel):
# 1. Direct default value
quantity: int = 1
# 2. Optional (can be None)
notes: Optional[str] = None
# 3. Using Field with default
priority: int = Field(default=5, ge=1, le=10)
What users can send:
# Minimal (uses all defaults):
{"name": "Pizza", "price": 9.99}
# Full (overrides defaults):
{
"name": "Pizza",
"price": 9.99,
"description": "Cheesy goodness",
"tax": 1.5
}
🪆 Nested Pydantic Models
Models Inside Models
Real data is often like Russian nesting dolls—boxes inside boxes!
class Address(BaseModel):
street: str
city: str
country: str
class User(BaseModel):
name: str
email: str
address: Address # Nested model!
The JSON would look like:
{
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "123 Main St",
"city": "Boston",
"country": "USA"
}
}
graph TD A[User Model] --> B[name: str] A --> C[email: str] A --> D[address: Address] D --> E[street: str] D --> F[city: str] D --> G[country: str]
Accessing Nested Data
@app.post("/users/")
async def create_user(user: User):
# Access nested fields easily!
city = user.address.city
return {"user_city": city}
📋 List and Dict Fields
Lists: Multiple Items
What if a user wants to order multiple pizzas?
from typing import List
class Order(BaseModel):
customer: str
items: List[str] # List of strings
JSON:
{
"customer": "Bob",
"items": ["Pizza", "Pasta", "Salad"]
}
Lists of Models
Even more powerful—lists of complex objects!
class Item(BaseModel):
name: str
price: float
class Order(BaseModel):
customer: str
items: List[Item] # List of Item models!
JSON:
{
"customer": "Bob",
"items": [
{"name": "Pizza", "price": 12.99},
{"name": "Pasta", "price": 8.99}
]
}
Dict Fields
For key-value data:
from typing import Dict
class Inventory(BaseModel):
products: Dict[str, int] # product name → quantity
JSON:
{
"products": {
"apple": 50,
"banana": 30,
"orange": 45
}
}
👨👩👧 Model Inheritance
Don’t Repeat Yourself!
If many models share fields, use inheritance!
class BaseItem(BaseModel):
name: str
description: Optional[str] = None
class Item(BaseItem):
price: float # Adds to base fields
class ItemWithTax(BaseItem):
price: float
tax: float # Different addition
Both Item and ItemWithTax have:
name(from BaseItem)description(from BaseItem)- Plus their own unique fields!
Real Example
class PersonBase(BaseModel):
name: str
email: str
class PersonCreate(PersonBase):
password: str # For creating accounts
class PersonPublic(PersonBase):
id: int # For returning data (no password!)
graph TD A[PersonBase] --> B[name: str] A --> C[email: str] D[PersonCreate] --> A D --> E[password: str] F[PersonPublic] --> A F --> G[id: int]
📝 Request Example Data
Show Examples in API Docs
FastAPI auto-generates beautiful docs. Make them even better with examples!
Method 1: Field Examples
class Item(BaseModel):
name: str = Field(example="Super Widget")
price: float = Field(example=29.99)
Method 2: model_config
class Item(BaseModel):
name: str
price: float
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Awesome Gadget",
"price": 49.99
}
]
}
}
Method 3: Multiple Examples
class Item(BaseModel):
name: str
price: float
model_config = {
"json_schema_extra": {
"examples": [
{"name": "Budget Phone", "price": 199.99},
{"name": "Premium Phone", "price": 999.99}
]
}
}
When you visit /docs, you’ll see these examples ready to test!
🎉 Putting It All Together
Here’s a complete, real-world example:
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import List, Optional
app = FastAPI()
class Address(BaseModel):
street: str = Field(min_length=5)
city: str
zip_code: str = Field(pattern=r"^\d{5}quot;)
class OrderItem(BaseModel):
product: str = Field(example="Pizza")
quantity: int = Field(ge=1, le=10, default=1)
price: float = Field(gt=0, example=9.99)
class Order(BaseModel):
customer_name: str = Field(min_length=2)
email: str
shipping: Address
items: List[OrderItem]
notes: Optional[str] = None
model_config = {
"json_schema_extra": {
"examples": [{
"customer_name": "Alice",
"email": "alice@email.com",
"shipping": {
"street": "123 Main Street",
"city": "Boston",
"zip_code": "02101"
},
"items": [
{"product": "Pizza", "quantity": 2, "price": 12.99}
],
"notes": "Ring doorbell"
}]
}
}
@app.post("/orders/")
async def create_order(order: Order):
total = sum(item.price * item.quantity for item in order.items)
return {
"message": f"Order received for {order.customer_name}",
"total": total
}
🚀 Key Takeaways
| Concept | What It Does |
|---|---|
| BaseModel | Creates data blueprints |
| Field() | Adds validation rules |
| Optional/defaults | Makes fields not required |
| Nested models | Models inside models |
| List/Dict | Multiple items or key-value pairs |
| Inheritance | Share fields between models |
| Examples | Show sample data in docs |
Remember: Pydantic is your data’s security guard. It checks everything at the door so your code only deals with clean, validated data! 🛡️