Once a CRUD flow is working, the next priority is making the response shape consistent. response_model lets FastAPI promise “this endpoint always returns JSON in this layout.” Declaring it ahead of time keeps the docs synchronized with the code and hides sensitive fields automatically. We will continue expanding the list-based API from Part 5 and apply the response_model option step by step.
Key terms
- response_model: An option that declares the JSON shape an endpoint returns. FastAPI filters the response and documents it automatically.
- Read/write model split: A pattern that creates an input-only model (
UserCreate) and an output-only model (UserRead) so secrets like passwords never leak. - Field: A Pydantic helper that adds descriptions, defaults, and constraints, improving both validation and Swagger docs.
- OpenAPI schema: The spec FastAPI generates. Changing
response_modelor the Pydantic models updates the shared contract instantly.
Practice card
- Estimated time: 40 minutes
- Prereqs: Part 5 CRUD example, access to Swagger UI
- Goal: Split write/read models and lock responses with
response_model
Core ideas
- Learn how to apply
response_model - Hide sensitive fields by separating write/read models
- Confirm that the docs update as soon as the schema changes
Separating input and output models
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class UserCreate(BaseModel):
email: str
password: str = Field(min_length=8)
class UserRead(BaseModel):
id: int
email: str
users: list[dict] = []
current_id = 0
UserCreate includes the password, while UserRead does not.
We are reusing the in-memory list from Part 5 for continuity—production apps should persist data in a database instead of mutable globals.
Set response_model
@app.post("/users", response_model=UserRead, status_code=201)
def create_user(payload: UserCreate):
global current_id
current_id += 1
record = {"id": current_id, **payload.model_dump()}
users.append(record)
return record
The returned dict still contains the password, but response_model=UserRead means the JSON response only exposes id and email. The same schema appears in the auto-generated docs so client developers do not guess.
How FastAPI filters the response
- FastAPI takes the Python object you return (dict, ORM row, etc.).
- It builds a new instance of the declared
response_model(UserRead). - Pydantic validates the data against the schema. Missing or mismatched fields raise a validation error.
- Only the validated fields are serialized to JSON.
This filtering phase happens at serialization time, so you should still avoid loading or storing secrets unnecessarily. Think of response_model as a guard rail, not a security boundary.
Visualize request and response models
Seeing both models in one diagram makes the “accept password but exclude it from the response” flow much easier to grasp.
Apply it to list responses
@app.get("/users", response_model=list[UserRead])
def list_users():
return users
Type hints make Swagger UI show clearly that the response is an array. Each dict inside users is filtered against UserRead, so even large lists respect the schema. For real workloads, add pagination so validation cost stays predictable.
Add field descriptions and examples
Field injects documentation with almost no extra code.
class UserCreate(BaseModel):
email: str = Field(description="Email used for login", examples=["[email protected]"])
password: str = Field(min_length=8, description="Password with at least 8 characters")
The examples value appears inside Swagger UI’s Example Value box.
Practical example: metadata wrapper
For school apps you might need to version responses. Adding an optional ApiEnvelope model lets older mobile builds detect outdated payloads without rewriting every endpoint.
class ApiEnvelope(BaseModel):
version: str = "v1"
data: list[UserRead]
@app.get("/users/enveloped", response_model=ApiEnvelope)
def list_users_with_meta():
return {"version": "v1", "data": users}
Most CRUD endpoints can return the core model directly. Use wrappers only when you have a concrete metadata or versioning requirement.
Response casting
FastAPI accepts dicts, ORM models, and more, then validates them against the declared response_model. Extra fields are removed, missing or mismatched types raise an error, and the resulting JSON mirrors the schema. You still control what leaves your persistence layer—response_model just guarantees the public contract.
from pydantic import BaseModel, ConfigDict
class UserRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
ConfigDict(from_attributes=True) (Pydantic v2) tells FastAPI it can read ORM attributes, just like class Config: orm_mode = True did in Pydantic v1.
Need to hide or show fields dynamically? Use response_model_exclude={"password"} or response_model_include={"id", "email"} alongside the main declaration.
Check it in the docs
Run the server and open /docs. POST /users now shows the UserRead schema under Response Body.
uv run fastapi dev main.py
Keeping docs and responses in sync makes collaboration far easier.
💬 댓글
이 글에 대한 의견을 남겨주세요