Skip to main content

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:

  1. Define the structure of your API responses
  2. Automatically convert database models to response schemas
  3. Filter out sensitive data from responses
  4. Generate accurate API documentation
  5. 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:

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

  1. Take the returned user_data dictionary
  2. Filter it through the UserResponse model
  3. Return only the fields defined in the model
  4. Convert the data to match the types defined in the model

The response will look like this:

json
{
"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:

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

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

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

python
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 password
  • UserResponse 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:

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

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

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

  1. Uses different models for creating and returning posts
  2. Handles nested model relationships (posts contain author information)
  3. Filters out sensitive or unnecessary information
  4. 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:

  1. Create a simple API for a todo list with different models for creating todos and returning them
  2. Build a user management API with appropriate response models that exclude sensitive information
  3. Create an API endpoint that returns different responses based on query parameters using Union types
  4. Design a product catalog API with nested categories and tags using response models

Additional Resources

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