FastAPI Extra Models
Introduction
When building real-world APIs, you'll often need to work with multiple related models rather than just a single data structure. FastAPI provides elegant ways to define, relate, and use multiple Pydantic models to create complex yet maintainable API schemas.
In this tutorial, we'll explore how to work with multiple models in FastAPI, establish relationships between them, and use them effectively in your API endpoints. This is a crucial skill for building robust applications with proper data separation and organization.
Why Use Multiple Models?
Before diving into the code, let's understand why we might need multiple models:
- Input vs Output: Often the data structure you receive might differ from what you return
- Data Validation: Different endpoints might require different validation rules
- Documentation: Multiple models help create clearer, more specific API documentation
- Security: You can exclude sensitive fields from response models
- Database Integration: Your database models might not match your API models
Basic Multiple Model Example
Let's start with a simple example of using different models for input and output:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import List, Optional
app = FastAPI()
class UserIn(BaseModel):
    username: str
    email: EmailStr
    password: str
    full_name: Optional[str] = None
class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn):
    # In a real app, you would save the user to a database here
    # Note that we return the input user, but FastAPI will filter
    # out the password field because we use UserOut as response_model
    return user
In this example:
- UserInincludes- passwordand is used for input validation
- UserOutexcludes- passwordand is used for the response
- The response_model=UserOutparameter ensures that FastAPI filters the output to matchUserOut
When testing with a request body like:
{
  "username": "johndoe",
  "email": "[email protected]",
  "password": "secret123",
  "full_name": "John Doe"
}
The API will return:
{
  "username": "johndoe",
  "email": "[email protected]",
  "full_name": "John Doe"
}
Advanced Model Relationships
Real applications often have complex relationships between models. Let's explore some common patterns:
Nested Models
You can define models that contain other models:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
class User(BaseModel):
    username: str
    full_name: Optional[str] = None
    email: str
    
class Order(BaseModel):
    items: List[Item]
    customer: User
@app.post("/orders/")
async def create_order(order: Order):
    return {"order_id": "123", "order": order}
With this structure, your API will expect a request body like:
{
  "items": [
    {
      "name": "Laptop",
      "description": "High-performance laptop",
      "price": 999.99,
      "tax": 81.00
    }
  ],
  "customer": {
    "username": "johndoe",
    "full_name": "John Doe",
    "email": "[email protected]"
  }
}
Union Type Models
Sometimes you might need to accept different types of models at the same endpoint. For example, a payment API might handle different payment methods:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
app = FastAPI()
class CreditCardPayment(BaseModel):
    card_number: str
    expiry_date: str
    security_code: str
    
class BankTransferPayment(BaseModel):
    account_number: str
    routing_number: str
    
class PaymentResponse(BaseModel):
    payment_id: str
    amount: float
    status: str
@app.post("/payments/", response_model=PaymentResponse)
async def process_payment(payment: Union[CreditCardPayment, BankTransferPayment]):
    # Process payment based on type
    if isinstance(payment, CreditCardPayment):
        # Process credit card
        payment_method = "credit_card"
    else:
        # Process bank transfer
        payment_method = "bank_transfer"
        
    return {
        "payment_id": "pay_123456",
        "amount": 100.50,
        "status": f"Processing {payment_method} payment"
    }
Model Inheritance
Pydantic models support inheritance, which is great for when you have models that share many fields:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()
class ItemBase(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    
class ItemCreate(ItemBase):
    tax: Optional[float] = None
    
class ItemInDB(ItemBase):
    id: int
    stock: int
    
@app.post("/items/", response_model=ItemInDB)
async def create_item(item: ItemCreate):
    # Simulate creating an item in database and returning it
    return ItemInDB(
        id=1,
        stock=20,
        name=item.name,
        description=item.description,
        price=item.price
    )
Using Extra Models for Database Operations
When working with databases, it's common to have distinct models for:
- Creating records (with validation)
- Reading records (for responses)
- Updating records (with partial data)
- Database models (ORM integration)
Here's an example using SQLAlchemy with FastAPI:
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
# Database setup (simplified)
from database import get_db, Base
from sqlalchemy import Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
app = FastAPI()
# SQLAlchemy model
class ProductDB(Base):
    __tablename__ = "products"
    
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(String)
    price = Column(Float)
    stock = Column(Integer)
# Pydantic models
class ProductBase(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    
class ProductCreate(ProductBase):
    stock: int
    
class ProductUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    price: Optional[float] = None
    stock: Optional[int] = None
class Product(ProductBase):
    id: int
    stock: int
    
    class Config:
        orm_mode = True
@app.post("/products/", response_model=Product)
def create_product(product: ProductCreate, db: Session = Depends(get_db)):
    db_product = ProductDB(**product.dict())
    db.add(db_product)
    db.commit()
    db.refresh(db_product)
    return db_product
@app.get("/products/{product_id}", response_model=Product)
def read_product(product_id: int, db: Session = Depends(get_db)):
    db_product = db.query(ProductDB).filter(ProductDB.id == product_id).first()
    if db_product is None:
        raise HTTPException(status_code=404, detail="Product not found")
    return db_product
@app.put("/products/{product_id}", response_model=Product)
def update_product(product_id: int, product: ProductUpdate, db: Session = Depends(get_db)):
    db_product = db.query(ProductDB).filter(ProductDB.id == product_id).first()
    if db_product is None:
        raise HTTPException(status_code=404, detail="Product not found")
        
    update_data = product.dict(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_product, key, value)
        
    db.commit()
    db.refresh(db_product)
    return db_product
In this example:
- ProductBasecontains common fields
- ProductCreateis used for creating new products
- ProductUpdatemakes all fields optional for partial updates
- Productis the response model with all fields including the ID
- ProductDBis the SQLAlchemy ORM model
Using Generic Models
For APIs with common response patterns, you can create generic models to reduce repetition:
from fastapi import FastAPI
from pydantic import BaseModel, Generic, TypeVar
from typing import Optional, List, Dict, Any
T = TypeVar('T')
app = FastAPI()
class User(BaseModel):
    id: int
    name: str
    email: str
class Item(BaseModel):
    id: int
    name: str
    price: float
class PaginatedResponse(BaseModel, Generic[T]):
    items: List[T]
    total: int
    page: int
    size: int
    
class GenericResponse(BaseModel, Generic[T]):
    data: T
    status: str
    message: Optional[str] = None
    meta: Optional[Dict[str, Any]] = None
@app.get("/users/", response_model=PaginatedResponse[User])
async def list_users():
    # Simulate database fetch
    users = [
        User(id=1, name="Alice", email="[email protected]"),
        User(id=2, name="Bob", email="[email protected]"),
    ]
    return PaginatedResponse(
        items=users,
        total=2,
        page=1,
        size=10
    )
@app.get("/items/{item_id}", response_model=GenericResponse[Item])
async def get_item(item_id: int):
    # Simulate database fetch
    item = Item(id=item_id, name="Laptop", price=999.99)
    return GenericResponse(
        data=item,
        status="success",
        message="Item retrieved successfully",
        meta={"accessed_at": "2023-07-15T15:30:00Z"}
    )
This approach provides consistent responses across different endpoint types while maintaining type safety.
Summary
Working with multiple models in FastAPI offers several advantages:
- Security: Keep sensitive data out of responses
- Flexibility: Handle different input and output requirements
- Organization: Create logical separations between your data models
- Maintainability: Update input validation without affecting output models
- Documentation: Generate clear API documentation with specific schemas
By properly using extra models, you can create more robust, secure, and maintainable API designs that handle complex real-world requirements.
Exercises
To reinforce your understanding of extra models in FastAPI:
- Create an e-commerce API with models for products, customers, and orders with appropriate relationships between them.
- Implement a blog system with models for users, posts, and comments using inheritance to reduce code duplication.
- Design an authentication system with separate models for registration, login, and user profiles.
- Create a generic pagination response model and apply it to several endpoints.
Additional Resources
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!