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​