FastAPI Response Models
When building APIs, handling responses properly is just as important as validating incoming requests. FastAPI provides powerful response modeling capabilities that help you create consistent, well-documented API endpoints. In this tutorial, we'll explore FastAPI response models and how they can improve your API development workflow.
What Are Response Models?
Response models in FastAPI allow you to:
- Define the structure of your API responses
- Automatically convert database models to response schemas
- Filter out sensitive data from responses
- Generate accurate API documentation
- Apply data validation for outgoing responses
At their core, response models are Pydantic models that FastAPI uses to shape your API's output data.
Basic Response Model Usage
Let's start with a simple example:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Define a response model
class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    is_active: bool
# Use the response model with an endpoint
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    # In a real app, you'd fetch this from a database
    user_data = {
        "id": user_id,
        "username": "johndoe",
        "email": "[email protected]",
        "is_active": True,
        "password": "secret-password",  # This will be filtered out!
        "admin_note": "VIP customer"    # This will be filtered out!
    }
    return user_data
When you call this endpoint with GET /users/123, FastAPI will:
- Take the returned user_datadictionary
- Filter it through the UserResponsemodel
- Return only the fields defined in the model
- Convert the data to match the types defined in the model
The response will look like this:
{
  "id": 123,
  "username": "johndoe",
  "email": "[email protected]",
  "is_active": true
}
Notice that the password and admin_note fields were automatically excluded because they're not part of our response model!
Excluding Fields with Response Models
You can explicitly control which fields to include or exclude using Pydantic's configuration options:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import Optional
app = FastAPI()
class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None
    is_active: bool = True
# Model for creating users (includes password)
class UserCreate(UserBase):
    password: str
# Model for responses (excludes password)
class UserResponse(UserBase):
    id: int
    
    class Config:
        schema_extra = {
            "example": {
                "id": 42,
                "username": "johndoe",
                "email": "[email protected]",
                "full_name": "John Doe",
                "is_active": True
            }
        }
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # Simulate user creation
    new_user = user.dict()
    new_user["id"] = 42  # In real app, this would be generated
    
    # Return user data (this includes password!)
    return new_user
In this example, even though our endpoint returns the complete user data (including password), FastAPI automatically filters it through the UserResponse model, removing the password field.
Response Model With response_model_exclude and response_model_include
FastAPI provides additional parameters to fine-tune response models:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
    id: int
    name: str
    description: str
    price: float
    tax: float
    tags: List[str]
@app.get(
    "/items/{item_id}/basic",
    response_model=Item,
    response_model_exclude={"tax", "tags"}
)
async def get_item_basic(item_id: int):
    return {
        "id": item_id,
        "name": "Fancy Item",
        "description": "A fancy item with great features",
        "price": 29.99,
        "tax": 5.99,  # This will be excluded
        "tags": ["fancy", "item"]  # This will be excluded
    }
@app.get(
    "/items/{item_id}/partial",
    response_model=Item,
    response_model_include={"name", "price"}
)
async def get_item_partial(item_id: int):
    return {
        "id": item_id,  # This will be excluded
        "name": "Fancy Item",
        "description": "A fancy item with great features",  # This will be excluded
        "price": 29.99,
        "tax": 5.99,  # This will be excluded
        "tags": ["fancy", "item"]  # This will be excluded
    }
The first endpoint excludes the tax and tags fields, while the second endpoint only includes the name and price fields.
Response Models with Nested Data
Response models can also handle nested data structures:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Tag(BaseModel):
    id: int
    name: str
class Category(BaseModel):
    id: int
    name: str
    
class Product(BaseModel):
    id: int
    name: str
    price: float
    description: Optional[str] = None
    category: Category
    tags: List[Tag]
@app.get("/products/{product_id}", response_model=Product)
async def get_product(product_id: int):
    # Simulate fetching a product from database
    return {
        "id": product_id,
        "name": "Smartphone",
        "price": 699.99,
        "description": "Latest model with advanced features",
        "category": {
            "id": 1,
            "name": "Electronics"
        },
        "tags": [
            {"id": 1, "name": "Tech"},
            {"id": 2, "name": "Mobile"},
        ],
        "internal_notes": "High profit margin item"  # This will be filtered out
    }
This endpoint will return a properly structured product with its category and tags, while excluding the internal_notes field.
Using Different Models for Input and Output
A common pattern in API development is to use different models for input and output:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, Optional
import datetime
app = FastAPI()
# Simulated database
fake_user_db: Dict[int, dict] = {}
# Input model
class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(..., min_length=8)
    full_name: Optional[str] = None
# Output model
class UserResponse(BaseModel):
    id: int
    username: str
    email: EmailStr
    full_name: Optional[str] = None
    created_at: datetime.datetime
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # Check if username exists
    for existing_user in fake_user_db.values():
        if existing_user["username"] == user.username:
            raise HTTPException(status_code=400, detail="Username already registered")
    
    # Create new user
    user_id = len(fake_user_db) + 1
    user_data = user.dict()
    created_user = {
        "id": user_id,
        "created_at": datetime.datetime.now(),
        **user_data
    }
    
    # Store in "database"
    fake_user_db[user_id] = created_user
    
    # Return user data (password will be filtered out by response_model)
    return created_user
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    if user_id not in fake_user_db:
        raise HTTPException(status_code=404, detail="User not found")
    return fake_user_db[user_id]
In this example:
- UserCreatevalidates incoming data and requires a password
- UserResponsedefines what data to send back, excluding sensitive information
- Both models enforce their own validation rules
Response Models with Lists
You can also use response models with lists of items:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Item(BaseModel):
    id: int
    name: str
    price: float
@app.get("/items/", response_model=List[Item])
async def get_items():
    # Simulate fetching items from a database
    items = [
        {"id": 1, "name": "Item 1", "price": 50.2, "stock": 10},
        {"id": 2, "name": "Item 2", "price": 30.0, "stock": 20},
        {"id": 3, "name": "Item 3", "price": 45.5, "stock": 5},
    ]
    return items  # stock will be filtered out from each item
This endpoint returns a list of items, each filtered through the Item model.
Using Union Types with Response Models
Sometimes an endpoint might return different response types. You can use Union types to handle this:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
app = FastAPI()
class UserProfile(BaseModel):
    user_id: int
    name: str
    bio: str
class CompanyProfile(BaseModel):
    company_id: int
    name: str
    description: str
    employee_count: int
@app.get("/profiles/{profile_id}", response_model=Union[UserProfile, CompanyProfile])
async def get_profile(profile_id: int, is_company: bool = False):
    if is_company:
        return {
            "company_id": profile_id,
            "name": "Acme Corp",
            "description": "A leading technology company",
            "employee_count": 100,
            "founded_year": 1995  # Will be excluded
        }
    else:
        return {
            "user_id": profile_id,
            "name": "John Doe",
            "bio": "Software developer and tech enthusiast",
            "email": "[email protected]"  # Will be excluded
        }
FastAPI will handle the validation based on the actual data structure returned.
Real-world Example: Blog API with Response Models
Here's a more complete example of a blog API using response models:
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import List, Optional
import datetime
app = FastAPI()
# --- Models ---
class PostBase(BaseModel):
    title: str = Field(..., min_length=3, max_length=100)
    content: str = Field(..., min_length=10)
    
class PostCreate(PostBase):
    pass
class PostUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=3, max_length=100)
    content: Optional[str] = Field(None, min_length=10)
class Author(BaseModel):
    id: int
    name: str
    
class PostResponse(PostBase):
    id: int
    created_at: datetime.datetime
    updated_at: Optional[datetime.datetime] = None
    author: Author
    
    class Config:
        schema_extra = {
            "example": {
                "id": 1,
                "title": "FastAPI Tutorial",
                "content": "This is a comprehensive guide to FastAPI...",
                "created_at": "2023-01-15T10:00:00",
                "author": {
                    "id": 1,
                    "name": "John Doe"
                }
            }
        }
# --- Simulated database ---
posts_db = [
    {
        "id": 1,
        "title": "Introduction to FastAPI",
        "content": "FastAPI is a modern web framework for building APIs with Python...",
        "created_at": datetime.datetime(2023, 1, 10, 12, 0),
        "updated_at": None,
        "author_id": 1,
        "draft": False
    }
]
users_db = [
    {
        "id": 1,
        "name": "John Doe",
        "email": "[email protected]",
        "password": "secret"
    }
]
# --- Helper functions ---
def get_user(user_id: int):
    for user in users_db:
        if user["id"] == user_id:
            return user
    return None
def get_post(post_id: int):
    for post in posts_db:
        if post["id"] == post_id:
            return post
    return None
# --- API Endpoints ---
@app.get("/posts/", response_model=List[PostResponse])
async def get_all_posts():
    # Enhance posts with author information
    result = []
    for post in posts_db:
        if not post["draft"]:  # Skip draft posts
            author = get_user(post["author_id"])
            post_with_author = {
                **post,
                "author": {
                    "id": author["id"],
                    "name": author["name"]
                }
            }
            result.append(post_with_author)
    return result
@app.get("/posts/{post_id}", response_model=PostResponse)
async def get_post_by_id(post_id: int):
    post = get_post(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Post not found")
    
    if post["draft"]:
        raise HTTPException(status_code=403, detail="Cannot access draft post")
        
    author = get_user(post["author_id"])
    post_with_author = {
        **post,
        "author": {
            "id": author["id"],
            "name": author["name"]
        }
    }
    return post_with_author
@app.post("/posts/", response_model=PostResponse, status_code=201)
async def create_post(post: PostCreate, author_id: int = 1):
    # Check if author exists
    author = get_user(author_id)
    if not author:
        raise HTTPException(status_code=404, detail="Author not found")
        
    # Create new post
    new_post = {
        "id": len(posts_db) + 1,
        "title": post.title,
        "content": post.content,
        "created_at": datetime.datetime.now(),
        "updated_at": None,
        "author_id": author_id,
        "draft": False
    }
    
    posts_db.append(new_post)
    
    # Return with author info
    return {
        **new_post,
        "author": {
            "id": author["id"],
            "name": author["name"]
        }
    }
In this example, we've created a blog API that:
- Uses different models for creating and returning posts
- Handles nested model relationships (posts contain author information)
- Filters out sensitive or unnecessary information
- Provides proper documentation with examples
Summary
Response models in FastAPI offer powerful capabilities to shape your API's output data:
- They help you create consistent API responses
- They automatically filter out sensitive data
- They convert data to the expected types
- They validate outgoing data
- They generate accurate API documentation
By separating your input and output models, you can create cleaner, more secure, and better-documented APIs. Response models work with FastAPI's automatic documentation generation to provide clear examples and expectations for your API consumers.
Exercises
To practice what you've learned:
- Create a simple API for a todo list with different models for creating todos and returning them
- Build a user management API with appropriate response models that exclude sensitive information
- Create an API endpoint that returns different responses based on query parameters using Union types
- Design a product catalog API with nested categories and tags using response models
Additional Resources
- FastAPI Official Documentation on Response Models
- Pydantic Documentation
- More advanced data filtering with Pydantic
Remember that response models are one of FastAPI's most powerful features for creating clean, secure, and well-documented APIs. They're worth mastering as you develop your FastAPI skills!
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!