Skip to main content

Response Model and Status Codes

The response_model parameter tells FastAPI exactly what shape your API response will have. It filters sensitive fields, generates OpenAPI schemas, and validates your handler's return value — preventing accidental data leaks.

Learning Focus

By the end of this lesson you can: declare response_model on route decorators, filter fields with response_model_exclude, set correct HTTP status codes, and understand how FastAPI validates response data before sending.

Declaring response_model

app/routers/users.py
from fastapi import APIRouter
from pydantic import BaseModel

class UserCreate(BaseModel):
username: str
email: str
password: str # Sensitive — should never be returned

class UserResponse(BaseModel):
id: int
username: str
email: str
# Note: no password field

router = APIRouter(prefix="/users", tags=["users"])

@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate) -> dict:
# Even if this dict has "password", response_model will strip it
return {
"id": 1,
"username": user.username,
"email": user.email,
"password": user.password, # This will be filtered out
}

The response sent to the client will never contain password, regardless of what your handler returns.

Returning the Model Directly

For clarity and type safety, return the Pydantic model instance directly:

app/routers/items.py
from fastapi import APIRouter
from pydantic import BaseModel

class ItemResponse(BaseModel):
id: int
name: str
price: float

@router.get("/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int) -> ItemResponse:
return ItemResponse(id=item_id, name="Widget", price=9.99)

With from_attributes=True in the model config, you can also return SQLAlchemy ORM objects directly.

HTTP Status Codes

OperationStatusUse
Successful GET200Default
Successful POST / creation201status_code=201
Accepted but not yet done202Long-running async tasks
Deleted with no body204status_code=204, return None
Redirect301/302RedirectResponse
Client error4xxRaise HTTPException
Server error5xxRaise or let unhandled exceptions bubble
app/routers/items.py
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response

@router.delete("/{item_id}", status_code=204)
async def delete_item(item_id: int) -> None:
if item_id not in db:
raise HTTPException(404, detail="Item not found")
del db[item_id]
# Return None — FastAPI sends empty 204 body

response_model_exclude and response_model_include

Filter fields at the decorator level without defining a new model:

app/routers/users.py
@router.get("/", response_model=list[UserResponse], response_model_exclude={"email"})
async def list_users() -> list[dict]:
return [{"id": 1, "username": "alice", "email": "alice@example.com"}]
# Email will not appear in the response
warning

Prefer separate models (UserPublic, UserAdmin) over response_model_exclude for complex APIs. Explicit models are easier to test, document, and maintain.

Multiple Response Codes in OpenAPI

Document multiple possible responses:

app/routers/items.py
from fastapi import APIRouter
from pydantic import BaseModel

class ErrorDetail(BaseModel):
detail: str

@router.get(
"/{item_id}",
response_model=ItemResponse,
responses={
404: {"model": ErrorDetail, "description": "Item not found"},
422: {"description": "Validation error"},
},
)
async def get_item(item_id: int) -> ItemResponse:
...

response_model_exclude_unset

When returning partial updates, exclude fields that were not explicitly set:

app/routers/items.py
class ItemUpdate(BaseModel):
name: str | None = None
price: float | None = None

@router.patch("/{item_id}", response_model=ItemUpdate, response_model_exclude_unset=True)
async def patch_item(item_id: int, item: ItemUpdate) -> ItemUpdate:
# Only fields the client sent will appear in the response
return item

Common Pitfalls

PitfallCause / SymptomFix
Sensitive field leaking in responseNo response_model or wrong model usedAlways set response_model for endpoints that return user data
204 response with bodyReturning a value for status_code=204Return None (or Response(status_code=204)) for 204
OpenAPI showing wrong schemaHandler return type hint differs from response_modelMatch the response_model to what the handler actually returns
Validation error on responseHandler returns data that fails response_model validationEnsure handler output matches the model — check computed fields
response_model_exclude not working on nestedExcluding a nested field pathUse a separate output model instead

Hands-On Practice

app/routers/accounts.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr, Field, ConfigDict

class AccountCreate(BaseModel):
username: str = Field(..., min_length=3)
email: EmailStr
password: str = Field(..., min_length=8)
bio: str | None = None

class AccountPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
bio: str | None

class AccountAdmin(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: str
bio: str | None

router = APIRouter(prefix="/accounts", tags=["accounts"])
_store: dict[int, dict] = {}
_counter = 0

@router.post("/", response_model=AccountPublic, status_code=201)
async def create_account(data: AccountCreate) -> dict:
global _counter
_counter += 1
record = {"id": _counter, "username": data.username, "email": data.email,
"bio": data.bio, "password_hash": "hashed:" + data.password}
_store[_counter] = record
return record # password_hash and email not in AccountPublic — filtered out

@router.get("/{account_id}", response_model=AccountPublic)
async def get_account_public(account_id: int) -> dict:
if account_id not in _store:
raise HTTPException(404, "Account not found")
return _store[account_id]
test-accounts.sh
uvicorn app.routers.accounts:router --reload --port 8006

curl -s -X POST http://localhost:8006/accounts/ \
-H "Content-Type: application/json" \
-d '{"username": "alice", "email": "alice@example.com", "password": "S3cretPass"}'
# → {"id": 1, "username": "alice", "bio": null} — no email, no password

curl http://localhost:8006/accounts/1

What's Next