FastAPI Schema Customization
Introduction
When building APIs with FastAPI, the schema system based on Pydantic models is one of its most powerful features. These schemas define the structure of requests and responses, providing automatic validation, serialization, and documentation. However, sometimes the default behavior doesn't exactly match your specific requirements.
This guide will show you how to customize FastAPI schemas to gain more control over:
- How your API is documented in the OpenAPI schema
- How data is validated
- How objects are serialized and deserialized
Whether you need to hide sensitive fields, add extra validation, or change how your data is presented, schema customization gives you the flexibility you need.
Understanding FastAPI Schemas
FastAPI uses Pydantic models to define schemas. These models automatically:
- Validate incoming data
- Convert (serialize/deserialize) between Python objects and JSON
- Generate OpenAPI documentation
Here's a basic example of a schema:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True
@app.post("/users/")
def create_user(user: User):
return user
While this works great for simple cases, real-world applications often need more customization.
Basic Schema Customization Techniques
Field Customization
Let's start with customizing fields using Pydantic's Field
function:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class User(BaseModel):
id: int = Field(..., gt=0, description="The user ID must be positive")
name: str = Field(..., min_length=2, max_length=50, example="John Doe")
email: str = Field(..., regex=r"^\S+@\S+\.\S+$")
is_active: bool = Field(True, description="Whether the user is active")
@app.post("/users/")
def create_user(user: User):
return user
In this example:
...
indicates a required field- We've added validation constraints (gt, min_length, max_length, regex)
- We've provided descriptions and examples for the documentation
Customizing Field Names
Sometimes your API should use different field names than your internal models:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class User(BaseModel):
user_id: int = Field(..., alias="id")
full_name: str = Field(..., alias="name")
email_address: str = Field(..., alias="email")
class Config:
populate_by_name = True
@app.post("/users/")
def create_user(user: User):
# Access fields by their Python names
print(user.user_id, user.full_name, user.email_address)
return user
In this example:
- External JSON will use
id
,name
, andemail
- Internal Python code will use
user_id
,full_name
, andemail_address
populate_by_name = True
allows either name to be used when creating the model
Advanced Schema Customization
Excluding Fields from Response
To exclude certain fields (like passwords) from responses:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: str
class UserOut(BaseModel):
username: str
email: str
@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn):
# Process the user, including password
# ...
return user # FastAPI will filter out password automatically
Here, we're defining separate input and output schemas to control what data gets returned.
Dynamic Field Inclusion/Exclusion
You can also dynamically include or exclude fields:
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str
price: float
tax: float
tags: List[str] = []
@app.get("/items/{item_id}")
def read_item(
item_id: int,
fields: List[str] = Query(None)
):
# Fetch item (example)
item = Item(
name="Foo",
description="A very nice item",
price=35.4,
tax=3.2,
tags=["tag1", "tag2"]
)
if fields:
# Convert to dict and keep only requested fields
response = {}
item_dict = item.model_dump()
for field in fields:
if field in item_dict:
response[field] = item_dict[field]
return response
return item
This example allows API consumers to specify which fields they want to receive.
Schema Example Customization
To provide better examples in your API documentation:
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(..., example="Smartphone")
description: str = Field(None, example="A high-end smartphone with great camera")
price: float = Field(..., example=799.99)
tax: float = Field(None, example=79.99)
tags: List[str] = Field(default=[], example=["electronics", "gadgets"])
class Config:
json_schema_extra = {
"example": {
"name": "Laptop",
"description": "Powerful development machine",
"price": 1299.99,
"tax": 129.99,
"tags": ["electronics", "computers"]
}
}
@app.post("/items/")
def create_item(item: Item):
return item
In this example:
- We provide field-level examples
- We also provide a complete model example in
json_schema_extra
Customizing Schema Based on Role or Context
Sometimes you need different schemas based on the user's role or context:
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel
from typing import Optional, Union
app = FastAPI()
# Simplified auth
def get_user_role():
# In real app, get from token/session
return "admin" # or "user"
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
price: float
class ItemUserView(ItemBase):
pass
class ItemAdminView(ItemBase):
cost_price: float
profit_margin: float
@app.get("/items/{item_id}", response_model=Union[ItemAdminView, ItemUserView])
def get_item(item_id: int, role: str = Depends(get_user_role)):
# Fetch item logic here
base_item = {
"name": "Fancy Item",
"description": "A fancy item indeed",
"price": 49.99
}
if role == "admin":
# Return with admin fields
return ItemAdminView(
**base_item,
cost_price=29.99,
profit_margin=20.00
)
else:
# Return user view
return ItemUserView(**base_item)
This approach allows different users to see different data based on their roles.
Customizing Validation and Error Messages
To provide custom error messages during validation:
from fastapi import FastAPI
from pydantic import BaseModel, Field, validator
app = FastAPI()
class User(BaseModel):
id: int
username: str
password: str
@validator("username")
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError("Username must be alphanumeric")
return v
@validator("password")
def password_strength(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(char.isdigit() for char in v):
raise ValueError("Password must contain at least one number")
if not any(char.isupper() for char in v):
raise ValueError("Password must contain at least one uppercase letter")
return v
@app.post("/users/")
def create_user(user: User):
return {"username": user.username, "id": user.id}
This example uses Pydantic's validator to implement custom validation logic and error messages.
Real-world Example: E-commerce API
Let's put everything together with a realistic e-commerce API endpoint:
from datetime import datetime
from typing import List, Optional
from fastapi import FastAPI, Query, Path, Body
from pydantic import BaseModel, Field, validator, AnyHttpUrl
app = FastAPI()
class ProductImage(BaseModel):
url: AnyHttpUrl
alt: Optional[str] = None
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
price: float = Field(..., gt=0, description="Price must be greater than zero")
category: str
class ProductCreate(ProductBase):
sku: str = Field(..., min_length=5, max_length=20, regex=r"^[A-Z0-9-]+$")
stock_count: int = Field(..., ge=0)
cost_price: float = Field(..., gt=0)
images: List[ProductImage] = []
@validator("price")
def price_must_exceed_cost(cls, v, values):
if "cost_price" in values and v <= values["cost_price"]:
raise ValueError("Selling price must be higher than cost price")
return v
class Config:
json_schema_extra = {
"example": {
"name": "Stylish T-Shirt",
"description": "A comfortable cotton t-shirt",
"price": 29.99,
"category": "clothing",
"sku": "TSHIRT-123",
"stock_count": 100,
"cost_price": 15.00,
"images": [
{"url": "https://example.com/tshirt-front.jpg", "alt": "Front view"},
{"url": "https://example.com/tshirt-back.jpg", "alt": "Back view"}
]
}
}
class ProductResponse(ProductBase):
id: int
sku: str
stock_status: str
images: List[ProductImage] = []
created_at: datetime
@validator("stock_status", pre=True)
def calculate_stock_status(cls, v, values):
# This would typically come from the database
# But for this example, we're calculating it
stock_count = getattr(values.get("_obj"), "stock_count", 0)
if stock_count > 20:
return "In Stock"
elif stock_count > 0:
return "Low Stock"
else:
return "Out of Stock"
class Config:
orm_mode = True # For ORM compatibility
@app.post("/products/", response_model=ProductResponse)
def create_product(product: ProductCreate):
# In a real app, you'd save to database
# For this example, we simulate creating a product
db_product = {
"id": 123,
"created_at": datetime.now(),
**product.model_dump(),
# We need to store stock_count even though it's not in response
# to calculate stock_status
"_obj": product # Hack for our validator example
}
return db_product
This comprehensive example shows:
- Nested models (ProductImage inside Product)
- Different models for create vs response
- Custom validation with dependencies between fields
- Field constraints and regex validation
- ORM mode for database integration
- Dynamic derived fields (stock_status)
- Rich documentation examples
Summary
FastAPI's schema customization offers powerful tools to control how your API behaves and appears to clients. By leveraging Pydantic models and FastAPI's features, you can:
- Validate data with precise constraints and custom logic
- Control which fields are visible in responses
- Provide clear documentation and examples
- Create different views of the same data for different contexts
- Implement complex business logic in your validation rules
These customization options help you build APIs that are not just functional, but also secure, user-friendly, and well-documented.
Additional Resources
Exercises
-
Create a User schema with fields for username, email, password, and date of birth. Add appropriate validations (password strength, adult age check, etc.)
-
Build a "blog post" API with different schemas for drafts vs published posts, and different views for authors vs readers.
-
Enhance the e-commerce example above by adding product variants (size, color) as a nested model, with validation to ensure the variant price adjustments make sense.
-
Create an API endpoint that allows the client to specify which fields they want in the response using query parameters.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)