When we return a dict or a model from a route, FastAPI serializes it as-is. But that’s risky — what if we accidentally return the user’s hashed_password? That’s where response_model comes in. Think of it like a strainer between our function’s return value and the wire.
What response_model does
It re-validates and filters our return value against a Pydantic schema. Fields not declared in the model get dropped. Docs auto-update too.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
class UserIn(BaseModel):
email: EmailStr
password: str # incoming
class UserOut(BaseModel):
email: EmailStr
# no password field — it cannot leak
app = FastAPI()
@app.post("/users", response_model=UserOut)
def create_user(user: UserIn):
save_to_db(user)
return user # password is silently stripped
status_code
By default, 200 OK. For POST that creates a resource, we want 201 Created. For DELETE with no body, 204 No Content.
from fastapi import FastAPI, status
@app.post("/items", response_model=Item, status_code=status.HTTP_201_CREATED)
def create_item(item: ItemIn):
return save(item)
@app.delete("/items/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(id: int):
remove(id)
# don't return anything for 204
Use the status module — way more readable than raw integers.
response_model_exclude_unset
In simple language: only send back fields the user actually set. Useful for PATCH endpoints and large optional models.
class Profile(BaseModel):
name: str | None = None
bio: str | None = None
age: int | None = None
@app.get("/me", response_model=Profile, response_model_exclude_unset=True)
def me():
return {"name": "Manish"} # response is just {"name": "Manish"}, no nulls
There’s also response_model_exclude_none (drops fields that are explicitly None) and response_model_exclude={"field"} to drop specific fields.
Multiple response models (union)
Sometimes one route returns different shapes — say, a public profile or a private one based on auth.
from typing import Union
class PublicUser(BaseModel):
username: str
class PrivateUser(PublicUser):
email: EmailStr
last_login: str
@app.get("/users/{id}", response_model=Union[PrivateUser, PublicUser])
def get_user(id: int, is_owner: bool = False):
user = db.get(id)
return user if is_owner else {"username": user.username}
For different status codes returning different shapes (like an error model on 404), use the responses parameter:
@app.get(
"/items/{id}",
response_model=Item,
responses={404: {"model": ErrorMessage}},
)
def get_item(id: int):
...
Interview cheat sheet
response_modelfilters output and updates OpenAPI docs.- Use it to prevent leaking sensitive fields like
hashed_password. 201for create,204for delete-no-body,200for everything else.response_model_exclude_unset=Truefor clean PATCH responses.responses={...}documents alternate status codes / models.