Response Models
Learn how to define, validate, and document API responses using Pydantic models, ensuring type safety and consistent data structures.
Basic Response Models
Defining Response Models
Use response_model to specify the structure of your response:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.post("/items/", response_model=Item)
def create_item(item: Item):
return item
Response Model Benefits
- Data Filtering: Only return fields defined in the response model
- Validation: Ensure response data matches the expected structure
- Documentation: Automatic schema generation for API docs
- Type Safety: Type checking and editor autocomplete
Response Model Features
Excluding Fields
Filter out sensitive or unnecessary data:
class UserIn(BaseModel):
username: str
password: str
email: str
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: str
full_name: str | None = None
@app.post("/users/", response_model=UserOut)
def create_user(user: UserIn):
# Password is not included in response
return user
Response Model Exclude
Exclude specific fields dynamically:
@app.get(
"/items/{item_id}",
response_model=Item,
response_model_exclude={"tax"}
)
def read_item(item_id: int):
return items_db[item_id]
Response Model Include
Include only specific fields:
@app.get(
"/items/{item_id}",
response_model=Item,
response_model_include={"name", "price"}
)
def read_item(item_id: int):
return items_db[item_id]
Exclude Unset Values
Only return fields that were explicitly set:
@app.get(
"/items/{item_id}",
response_model=Item,
response_model_exclude_unset=True
)
def read_item(item_id: int):
# If tax was not set, it won't appear in response
return Item(name="Item", price=10.0)
Exclude Defaults
Exclude fields that have default values:
@app.get(
"/items/{item_id}",
response_model=Item,
response_model_exclude_defaults=True
)
def read_item(item_id: int):
return items_db[item_id]
Exclude None Values
Remove fields with None values:
@app.get(
"/items/{item_id}",
response_model=Item,
response_model_exclude_none=True
)
def read_item(item_id: int):
return items_db[item_id]
Multiple Response Models
Different Responses for Different Status Codes
from typing import Union
class ItemBase(BaseModel):
name: str
price: float
class ItemCreate(ItemBase):
pass
class ItemResponse(ItemBase):
id: int
created_at: str
class ErrorResponse(BaseModel):
error: str
detail: str
@app.post(
"/items/",
response_model=ItemResponse,
responses={
201: {"model": ItemResponse},
400: {"model": ErrorResponse}
}
)
def create_item(item: ItemCreate):
if item.price < 0:
raise HTTPException(
status_code=400,
detail="Price must be positive"
)
new_item = ItemResponse(
id=1,
name=item.name,
price=item.price,
created_at="2024-01-01"
)
return new_item
Union Response Types
from typing import Union
class SuccessResponse(BaseModel):
message: str
data: dict
class ErrorResponse(BaseModel):
error: str
@app.get("/items/{item_id}")
def read_item(item_id: int) -> Union[SuccessResponse, ErrorResponse]:
if item_id not in items_db:
return ErrorResponse(error="Item not found")
return SuccessResponse(message="Success", data=items_db[item_id])
List Responses
Returning Lists
from typing import List
@app.get("/items/", response_model=List[Item])
def read_items():
return items_db.values()
Paginated Lists
from typing import List
class PaginatedResponse(BaseModel):
items: List[Item]
total: int
page: int
page_size: int
@app.get("/items/", response_model=PaginatedResponse)
def read_items(page: int = 1, page_size: int = 10):
start = (page - 1) * page_size
end = start + page_size
items = list(items_db.values())[start:end]
return PaginatedResponse(
items=items,
total=len(items_db),
page=page,
page_size=page_size
)
Nested Response Models
Complex Nested Structures
from typing import List
class Tag(BaseModel):
name: str
class Image(BaseModel):
url: str
alt_text: str
class ItemDetail(BaseModel):
id: int
name: str
description: str
price: float
tags: List[Tag]
images: List[Image]
@app.get("/items/{item_id}", response_model=ItemDetail)
def read_item(item_id: int):
return ItemDetail(
id=item_id,
name="Laptop",
description="High-performance laptop",
price=1299.99,
tags=[Tag(name="electronics"), Tag(name="computers")],
images=[
Image(url="http://example.com/img1.jpg", alt_text="Front view"),
Image(url="http://example.com/img2.jpg", alt_text="Side view")
]
)
Recursive Models
class Category(BaseModel):
name: str
subcategories: List['Category'] = []
Category.model_rebuild() # Required for recursive models
@app.get("/categories/", response_model=List[Category])
def read_categories():
return [
Category(
name="Electronics",
subcategories=[
Category(name="Computers"),
Category(name="Phones")
]
),
Category(name="Clothing")
]
Response Customization
Custom JSON Encoder
from datetime import datetime
from fastapi.encoders import jsonable_encoder
class CustomItem(BaseModel):
name: str
created_at: datetime
class Config:
json_encoders = {
datetime: lambda v: v.strftime("%Y-%m-%d %H:%M:%S")
}
@app.get("/items/{item_id}", response_model=CustomItem)
def read_item(item_id: int):
return CustomItem(name="Item", created_at=datetime.now())
Field Aliases
from pydantic import Field
class Item(BaseModel):
name: str
item_price: float = Field(..., alias="price")
tax_amount: float | None = Field(None, alias="tax")
class Config:
populate_by_name = True
@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int):
# Response will use aliases: "price" and "tax"
return Item(name="Item", price=10.0, tax=1.5)
Response Status Codes
Setting Status Codes
from fastapi import status
@app.post("/items/", status_code=status.HTTP_201_CREATED)
def create_item(item: Item):
return item
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
return None
Dynamic Status Codes
from fastapi import Response
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item, response: Response):
if item_id in items_db:
response.status_code = status.HTTP_200_OK
items_db[item_id] = item
else:
response.status_code = status.HTTP_201_CREATED
items_db[item_id] = item
return item
Response Headers
Setting Response Headers
from fastapi import Response
@app.get("/items/{item_id}")
def read_item(item_id: int, response: Response):
response.headers["X-Custom-Header"] = "custom-value"
response.headers["X-Item-ID"] = str(item_id)
return {"item_id": item_id}
Custom Response Class
from fastapi.responses import JSONResponse
@app.get("/items/{item_id}")
def read_item(item_id: int):
return JSONResponse(
content={"item_id": item_id},
headers={"X-Custom-Header": "value"},
status_code=200
)
Different Response Types
JSON Response (Default)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id} # Automatically JSONResponse
Plain Text Response
from fastapi.responses import PlainTextResponse
@app.get("/text/", response_class=PlainTextResponse)
def read_text():
return "Hello, World!"
HTML Response
from fastapi.responses import HTMLResponse
@app.get("/html/", response_class=HTMLResponse)
def read_html():
return """
<html>
<head><title>FastAPI</title></head>
<body><h1>Hello, World!</h1></body>
</html>
"""
File Response
from fastapi.responses import FileResponse
@app.get("/download/{filename}")
def download_file(filename: str):
return FileResponse(
path=f"files/{filename}",
filename=filename,
media_type="application/octet-stream"
)
Streaming Response
from fastapi.responses import StreamingResponse
import io
@app.get("/stream/")
def stream_data():
def generate():
for i in range(100):
yield f"Line {i}\n"
return StreamingResponse(
generate(),
media_type="text/plain"
)
Redirect Response
from fastapi.responses import RedirectResponse
@app.get("/old-url")
def old_url():
return RedirectResponse(url="/new-url")
@app.get("/redirect/{item_id}")
def redirect_item(item_id: int):
return RedirectResponse(
url=f"/items/{item_id}",
status_code=status.HTTP_303_SEE_OTHER
)
Response Validation
Output Validation
from pydantic import BaseModel, validator
class ItemResponse(BaseModel):
name: str
price: float
final_price: float
@validator('final_price')
def validate_final_price(cls, v, values):
if v < 0:
raise ValueError('Final price cannot be negative')
if 'price' in values and v > values['price'] * 2:
raise ValueError('Final price too high')
return v
@app.get("/items/{item_id}", response_model=ItemResponse)
def read_item(item_id: int):
return ItemResponse(
name="Item",
price=100.0,
final_price=150.0
)
Generic Response Wrappers
Success/Error Wrapper
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class ResponseWrapper(BaseModel, Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[str] = None
message: str = ""
@app.get("/items/{item_id}", response_model=ResponseWrapper[Item])
def read_item(item_id: int):
if item_id not in items_db:
return ResponseWrapper(
success=False,
error="Item not found",
message="The requested item does not exist"
)
return ResponseWrapper(
success=True,
data=items_db[item_id],
message="Item retrieved successfully"
)
API Response Standard
from datetime import datetime
class ApiResponse(BaseModel, Generic[T]):
timestamp: datetime
status: str
data: T
metadata: dict = {}
@app.get("/items/{item_id}", response_model=ApiResponse[Item])
def read_item(item_id: int):
return ApiResponse(
timestamp=datetime.now(),
status="success",
data=items_db[item_id],
metadata={"version": "1.0"}
)
Response Compression
Enable Gzip Compression
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/large-response/")
def large_response():
# Response will be compressed if > 1000 bytes
return {"data": "x" * 10000}
Best Practices
- Use Response Models: Always define response models for type safety and documentation
- Consistent Structure: Use consistent response structures across your API
- Proper Status Codes: Use appropriate HTTP status codes (200, 201, 204, etc.)
- Error Responses: Define clear error response models
- Pagination: Implement pagination for list endpoints
- Field Filtering: Allow clients to specify which fields they need
- Versioning: Include API version in responses when needed
- Compression: Enable compression for large responses
- Caching Headers: Set appropriate cache headers
- Response Time: Monitor and optimize response times
Response models are a powerful feature in FastAPI that ensure your API returns consistent, well-documented, and validated data structures. Master these patterns to build reliable APIs.