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 (includespassword).UserPublic: what the client sees (no secrets).UserInDB: what you store (includeshashed_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.