Unit 06.00: The response model, and what it hides
The response model is a security control, not a formatting preference.
What the handler returned, and what left the building
A handler returning an internal object with a password hash, declared with a narrow response model.
The code shows both.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserOut(BaseModel):
id: str
name: str
class UserInternal(BaseModel):
id: str
name: str
password_hash: str
internal_notes: str
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: str):
return UserInternal(id=user_id, name="A Sharma",
password_hash="$2b$...", internal_notes="flagged")
client = TestClient(app)
print("returned by the handler : id, name, password_hash, internal_notes")
print("sent to the client :", list(TestClient(app).get("/users/1").json()))
print("\nThe response model filtered two fields the handler returned.")
# This is a security control, not a formatting one. Without `response_model`
# the handler's return value is serialised whole, and the password hash goes
# out with it.
The handler returned four fields and the client received two. Without the response model the return value is serialised whole, and the password hash and internal notes go out with it.
That is a realistic accident: an internal type gains a field, and every endpoint returning it starts leaking that field with no code change and no error.
The mistake this prevents
The mistake is returning the ORM or internal object directly and relying on it containing only safe fields. It contains only safe fields today; the response model is what keeps that true after the next migration.
Takeaway
Declare a response model on every endpoint. It filters what leaves, so a new internal field cannot silently become public.
