Response models and status codes

FastAPI · Essentials · 12 min

Response models stop you from leaking fields like password hashes and give clients a documented, stable contract.

FastAPI validates what comes in, and a response model controls what goes out. The simplest way is a return type annotation: def create_user(...) -> UserPublic. FastAPI validates the return value against UserPublic, filters it down to that model's fields, serializes it to JSON and documents the response schema.

Filtering is the important part. A common pattern is one model per direction:

  • UserCreate: what the client sends (includes password).
  • UserPublic: what the client sees (no secrets).
  • UserInDB: what you store (includes hashed_password).

Inherit them from a shared base to avoid repetition. If the function returns a UserInDB but is annotated -> UserPublic, hashed_password is dropped from the response.

When the annotation would be wrong for the type checker, for example you return a dict or an ORM object but want the model in the docs, put it on the decorator instead: @app.get("/users/{id}", response_model=UserPublic). response_model takes priority over the annotation, and response_model=None turns the feature off. response_model_exclude_unset=True omits fields that were never set, which keeps sparse resources compact.

Set the success status code on the decorator: status_code=201 when creating, 204 for an empty "deleted" response. The constants in fastapi.status (status.HTTP_201_CREATED) read better than bare numbers.

If your return value doesn't fit the response model, that's a bug on the server side, so FastAPI raises an error and the client gets a 500, not a 422. It's a useful safety net: a broken query can't silently ship malformed data.

Example

from fastapi import FastAPI, status
from fastapi.testclient import TestClient
from pydantic import BaseModel

class UserBase(BaseModel):
    username: str
    email: str

class UserCreate(UserBase):
    password: str

class UserPublic(UserBase):
    id: int

class UserInDB(UserPublic):
    hashed_password: str

app = FastAPI()

@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserCreate) -> UserPublic:
    data = user.model_dump(exclude={"password"})
    return UserInDB(id=1, hashed_password="hash-of-" + user.password, **data)

r = TestClient(app).post("/users", json={"username": "ann", "email": "a@x.io", "password": "s3cret"})
print(r.status_code, r.json())   # 201 {'username': 'ann', 'email': 'a@x.io', 'id': 1}

Remember

  • A return annotation or response_model validates, filters and documents the output
  • Use separate input, output and storage models so secrets never leave the server
  • response_model= on the decorator wins over the return annotation
  • Set success codes with status_code=, e.g. 201 for creation

Watch out for

  • Using the same model for input and output, so clients can set server-owned fields like id or see internal ones.

Check yourself

  1. The function is annotated -> UserPublic but returns a UserInDB that has hashed_password. What does the client receive?
  2. Your endpoint returns a value that doesn't match its response model. What does the client get?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: fastapi.tiangolo.com/tutorial/response-model/