Skip to main content

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

  1. Data Filtering: Only return fields defined in the response model
  2. Validation: Ensure response data matches the expected structure
  3. Documentation: Automatic schema generation for API docs
  4. 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

  1. Use Response Models: Always define response models for type safety and documentation
  2. Consistent Structure: Use consistent response structures across your API
  3. Proper Status Codes: Use appropriate HTTP status codes (200, 201, 204, etc.)
  4. Error Responses: Define clear error response models
  5. Pagination: Implement pagination for list endpoints
  6. Field Filtering: Allow clients to specify which fields they need
  7. Versioning: Include API version in responses when needed
  8. Compression: Enable compression for large responses
  9. Caching Headers: Set appropriate cache headers
  10. 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.