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_data
dictionary - Filter it through the
UserResponse
model - 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:
UserCreate
validates incoming data and requires a passwordUserResponse
defines 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!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)