Skip to main content

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:

  1. Validate incoming data
  2. Convert (serialize/deserialize) between Python objects and JSON
  3. Generate OpenAPI documentation

Here's a basic example of a schema:

python
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:

python
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:

python
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, and email
  • Internal Python code will use user_id, full_name, and email_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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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

  1. Create a User schema with fields for username, email, password, and date of birth. Add appropriate validations (password strength, adult age check, etc.)

  2. Build a "blog post" API with different schemas for drafts vs published posts, and different views for authors vs readers.

  3. 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.

  4. 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! :)