Response Models & Status Codes

intermediate fastapi pydantic http

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
response_model filtering
function returns
{email, password,
 hashed_pw, role}
UserOut filter
{email}
client sees
{"email": "..."}

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_model filters output and updates OpenAPI docs.
  • Use it to prevent leaking sensitive fields like hashed_password.
  • 201 for create, 204 for delete-no-body, 200 for everything else.
  • response_model_exclude_unset=True for clean PATCH responses.
  • responses={...} documents alternate status codes / models.