Difficulty: Intermediate
Response models in FastAPI control what data your API sends back to clients. While you can return any dictionary or Pydantic model from a handler, using the response_model parameter ensures that the output is validated, filtered, and documented according to a specific schema. This is one of FastAPI's most powerful features for building secure and well-documented APIs.
The response_model parameter is set on the route decorator: @app.get("/users/{id}", response_model=UserResponse). When specified, FastAPI takes whatever your handler returns and filters it through the response model. Only fields defined in the response model are included in the response. This means even if your database returns sensitive fields like password_hash, they will be automatically stripped if UserResponse does not include that field.
This filtering behavior is a critical security feature. In many applications, the internal data model (from the database) contains fields that should never be exposed to clients: password hashes, internal IDs, admin flags, API keys. By defining a response model that excludes these fields, you create a safe boundary between your internal data and the API output.
You typically create separate models for different operations: a Create model for input, a Response model for output, and an Update model for partial updates. For example, UserCreate might have name, email, and password. UserResponse might have id, name, and email (no password). UserUpdate might have optional name and email (for PATCH).
FastAPI also supports response_model_exclude and response_model_include parameters for quick field filtering without creating a separate model. However, creating explicit response models is preferred because they are reusable, self-documenting, and type-safe.
Response models also drive the API documentation. The Swagger UI shows the response schema based on the response_model, helping clients understand exactly what to expect. This is much more useful than showing a generic 'object' type.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Internal model (has sensitive fields)
class UserInDB(BaseModel):
id: int
name: str
email: str
password_hash: str
is_admin: bool
# Response model (safe for clients)
class UserResponse(BaseModel):
id: int
name: str
email: str
# Create model (input from client)
class UserCreate(BaseModel):
name: str
email: str
password: str
# Simulated database
fake_db: dict[int, UserInDB] = {
1: UserInDB(id=1, name="Alice", email="alice@example.com",
password_hash="$2b$12$abc...", is_admin=True),
}
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
user = fake_db.get(user_id)
if not user:
raise HTTPException(status_code=404)
# Returns full UserInDB, but response_model filters to UserResponse
return user
# Response: {"id": 1, "name": "Alice", "email": "alice@example.com"}
# password_hash and is_admin are automatically stripped!
Even though the handler returns a UserInDB with password_hash and is_admin, the response_model=UserResponse filters the output to only include id, name, and email. This prevents accidental data leaks.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserBrief(BaseModel):
id: int
name: str
class UserDetail(BaseModel):
id: int
name: str
email: str
bio: str | None
post_count: int
@app.get("/users", response_model=list[UserBrief])
def list_users():
"""Returns minimal info for listing"""
return [
{"id": 1, "name": "Alice", "email": "hidden", "bio": "hidden"},
{"id": 2, "name": "Bob", "email": "hidden", "bio": "hidden"},
]
@app.get("/users/{user_id}", response_model=UserDetail)
def get_user(user_id: int):
"""Returns full detail for a single user"""
return {
"id": user_id,
"name": "Alice",
"email": "alice@example.com",
"bio": "Software developer",
"post_count": 42,
}
Different endpoints can use different response models. The list endpoint returns minimal UserBrief data for efficiency. The detail endpoint returns full UserDetail. Swagger UI documents both response shapes separately.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float = 0.0
internal_code: str | None = None
# Exclude None values from response
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
def get_item(item_id: int):
return {"name": "Widget", "price": 9.99}
# Response: {"name": "Widget", "price": 9.99}
# description, tax, internal_code are excluded (not set)
# Exclude specific fields
@app.get(
"/items-safe/{item_id}",
response_model=Item,
response_model_exclude={"internal_code"},
)
def get_item_safe(item_id: int):
return {
"name": "Widget",
"price": 9.99,
"internal_code": "INTERNAL-123",
}
# Response: {"name": "Widget", "price": 9.99, ...}
# internal_code is excluded
# Include only specific fields
@app.get(
"/items-minimal/{item_id}",
response_model=Item,
response_model_include={"name", "price"},
)
def get_item_minimal(item_id: int):
return {"name": "Widget", "price": 9.99, "description": "A widget"}
# Response: {"name": "Widget", "price": 9.99}
response_model_exclude_unset=True omits fields that were not explicitly set (useful for sparse objects). response_model_exclude and response_model_include filter specific fields. Creating separate models is preferred over ad-hoc filtering.
response_model, Data Filtering, Exclude Fields, Response Schema, Documentation