Response Models

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.

Code examples

Filtering Sensitive Data

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.

Multiple Response Models

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.

Response Model Options

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.

Key points

Concepts covered

response_model, Data Filtering, Exclude Fields, Response Schema, Documentation