Declaring what comes back - which documents the endpoint and, more usefully, decides what cannot leak.
Overview
The default is generous
A response model describes the shape of what your endpoint sends back. You declare it once, and FastAPI uses it in three ways. It documents the response, it checks that your return value matches, and it removes anything that should not leave the server. That last job is the one people underestimate.
Without a response model, FastAPI sends whatever you return. That is generous, and it can be dangerous. Return a database record that includes a password hash, and the hash goes out with the rest. A response model is a filter that makes this kind of leak much harder to cause by accident.
This article begins with that generous default and why it deserves your attention. It then shows how to declare the shape and what changes in the generated docs. After that you will see how the framework checks your work, and what happens when your data does not fit the model you promised.
Trimming the output comes next. You will learn how to leave out fields that are unset, fields that hold default values, or fields you name directly. The article closes with a point that guides good API design: input and output are different questions, and they usually need different models.
If you have ever worried about what your API might expose, this is the article that gives you a dependable answer.
Think of the response model as the last gate before data leaves your server. Your database layer may hold far more than a client should see, such as internal flags, timestamps, or private notes. The model decides what passes through. It also gives clients a dependable promise about the shape of every reply. That promise is what lets frontend developers build against your API without guessing. Each section is short and ends with a habit you can apply straight away, so you finish with working knowledge and a safer API.
Without a response model, FastAPI serialises what your handler returns.
That is convenient and it's how data leaks. A handler that returns a DB row returns *all* of it. Inner notes, an email address, a password hash, a flag no one outside should know exists. Nothing warns you. From the framework's point of view you asked for just that.
The failure mode is quiet and it compounds. A column added to a table six months later silently starts appearing in an API response. The endpoint was never told what it was allowed to send.
Worth knowing
Without a response_model, whatever the handler returns is serialised — including keys that were never meant to leave.
With one, the return value is filtered through the model. Undeclared fields are dropped rather than sent.
A return-type annotation (-> ModuleOut) does the same job and usually reads better.
The response is validated too. A handler returning the wrong shape produces a 500, which is correct — it is your bug, not the caller's.
response_model_exclude_none, _exclude_unset and _exclude trim output without needing a second model.
Separate input and output models are the norm: what a caller may send and what they may see are different questions.
Response Models: Deciding What Leaves
Declaring the response docs the endpoint and, more usefully, stops things escaping.
Without one, whatever you return is sent
A handler returning an internal row sends every key in it, including the ones nobody outside should see.
example_01.pyFastAPI
from fastapi import FastAPI
app = FastAPI()
ROW = {"id": 1, "title": "Vectors", "minutes": 8,
"internal_notes": "needs a diagram",
"author_email": "ada@vizlearn.in"}
@app.get("/modules/1")
def read():
return ROW
print(TestClient(app).get("/modules/1").json())
print()
print("Two fields there were never meant for a caller.")
Output
response_model decides the shape
Declare it and the return value is filtered through the model. Anything not declared is dropped.
example_02.pyFastAPI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
ROW = {"id": 1, "title": "Vectors", "minutes": 8,
"internal_notes": "needs a diagram",
"author_email": "ada@vizlearn.in"}
class ModuleOut(BaseModel):
id: int
title: str
minutes: int
@app.get("/modules/1", response_model=ModuleOut)
def read():
return ROW # extra keys are removed, not sent
print(TestClient(app).get("/modules/1").json())
print()
print("The handler still returns the whole row. The model decides what leaves.")
Output
The return annotation works too
Annotating the return type does the same job and reads better. FastAPI treats it as the response model.
example_03.pyFastAPI
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ModuleOut(BaseModel):
id: int
title: str
DB = [{"id": 1, "title": "Vectors", "secret": "x"},
{"id": 2, "title": "Norms", "secret": "y"}]
@app.get("/modules")
def list_modules() -> List[ModuleOut]:
return DB
client = TestClient(app)
print(client.get("/modules").json())
print()
print("A list works the same way, filtered item by item.")
Output
It validates your own output
The response is checked against the model. A handler that returns the wrong shape fails loudly instead of shipping it.
example_04.pyFastAPI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ModuleOut(BaseModel):
id: int
title: str
@app.get("/good", response_model=ModuleOut)
def good():
return {"id": 1, "title": "Vectors"}
@app.get("/bad", response_model=ModuleOut)
def bad():
return {"id": "not-a-number", "title": "Vectors"}
client = TestClient(app)
print("good:", client.get("/good").status_code, client.get("/good").json())
try:
r = client.get("/bad")
print("bad :", r.status_code)
except Exception as e:
print("bad : raised", type(e).__name__)
print(" ", str(e).splitlines()[-1][:90])
print()
print("A broken response is a 500 - your bug, not the caller's.")
Output
Dropping unset and null fields
response_model_exclude_none and its siblings trim the output without a second model.
Separate models for what you accept and what you return — the pattern almost every real endpoint ends up with.
example_06.pyFastAPI
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
DB = {}
class ModuleCreate(BaseModel):
title: str = Field(min_length=3)
minutes: int = Field(default=10, gt=0)
class ModuleOut(BaseModel):
id: int
title: str
minutes: int
@app.post("/modules", response_model=ModuleOut, status_code=201)
def create(module: ModuleCreate):
new_id = len(DB) + 1
DB[new_id] = {"id": new_id, **module.model_dump(),
"created_by": "internal-service"}
return DB[new_id]
client = TestClient(app)
r = client.post("/modules", json={"title": "Eigenvalues", "minutes": 14})
print("returned:", r.json())
print("stored :", DB[1])
print()
print("created_by is in the store and not in the response.")
Now the return value is passed through ModuleOut. Fields the model declares are kept. All else is dropped.
The handler is unchanged — it still returns the whole row — but the model decides what leaves. That split is the handy part. Your data access can return what is natural. The contract with the outside world is stated once, in a class. Here It can be reviewed.
The modern spelling is a return annotation:
def list_modules() -> List[ModuleOut]:
Same action, and it reads as Python rather than as framework config. Use response_model= when the two need to differ. Returning a Response right, say, while still documenting the shape.
It checks your work
The response is checked against the model. This Is worth appreciating.
If a handler returns {"id": "not-a-number"} for a field declared int, that is a 500. The caller sent nothing wrong. Your code made a thing that doesn't match its own contract. The framework refuses to ship it.
That is the right action and it catches real bugs. A query returning a string where a number was expected, a field renamed in one place and not another, a None where the model promised a value. Without a response model, all of those reach the client and become their problem to diagnose.
Trimming the output
Three args handle the common adjustments without a second model.
response_model_exclude_none=True drops fields that are None. handy for a sparse response where nulls carry no data.
response_model_exclude_unset=True drops fields that were never set, keeping clear nulls. This is the one that matters for anything update-shaped, for the reasons the Pydantic track laid out.
response_model_exclude={"field"} drops named fields.
They're convenient and they're a slope. Once you're passing two of them plus an exclusion set, a dedicated output model is clearer, appears right in the schema. Can't silently start including a field that was renamed.
Input and output are different questions
The pattern almost each endpoint settles on is split models per direction.
Create takes what a caller may supply. No id. The server assigns it. No created_by, because the server knows it.
Out declares what a caller may see. It includes the id, excludes anything inner. Is needed to be full. By the time you're returning one, those fields exist.
Update has all optional, and is applied with exclude_unset.
Three small classes instead of one clever one. The temptation is always to reuse a single model with optional fields. It makes an API that docs nothing. Each field might be missing. No client can tell what is sure either way.
What it does for the documentation
The response model is what makes the made docs show the response, not just the request.
Without one, the OpenAPI doc says the endpoint returns "anything". each made client then makes an untyped result. Each consumer writes their own guess at the shape.
With one, the schema shows the response just. Clients get a real type. That is the gap between an API somebody can build against confidently and one they've to explore by trial.
Descriptions and examples on output fields are worth writing for the same reason they're on inputs. They appear in the docs next to the field.
A few practical notes
Status codes.response_model shows the success response. Error shapes are documented apart with responses={404: {...}}. This Is worth doing for an API with consumers.
Lists.-> List[ModuleOut] filters each item. There's no extra work for sets.
ORM objects. If the handler returns an ORM row rather than a dict, the output model needs from_attributes=True so it can read fields. That is the setting called orm_mode in Pydantic v1. It's the usual reason a response model raises when it looks like it should work.
speed. Filtering costs a check pass per response. It's cheap. For a large list it isn't free. But shipping fields you didn't intend to is a worse problem than a few microseconds.
The habit worth forming
Declare a response model on each endpoint that returns data, even when it looks same to what you're returning anyway.
The value isn't in today's filtering. It's that the endpoint now has a written contract. When a column is added to the underlying table next year, the response doesn't change. A thing declared what the response is.
Documenting the errors too
response_model shows the success case. An endpoint that can return a 404 says nothing about it by default. A made client has no idea what shape an error takes.
responses= fills the gap:
@app.get("/modules/{id}", response_model=ModuleOut,
responses={404: {"description": "Module not found"}})
For an API with consumers this is worth doing on each endpoint that can fail in a real way. It costs a line and it turns "you'll get a thing on failure" into a documented contract.
Status codes and the response model
The declared model shows the route's *default* status. A handler returning a other status with a other shape. A 202 with a job id rather than a 201 with the object. Is returning a thing the schema doesn't show.
responses= can doc those options with their own models. This Keeps the doc honest. The option, an endpoint whose real action is broader than its schema, is the thing that makes made clients untrustworthy.
Response models and inheritance
A common shape is a base model with the shared fields and variants that add to it:
class ModuleBase(BaseModel):
title: str
minutes: int
class ModuleOut(ModuleBase):
id: int
Worth knowing: if a field is annotated with the base and the handler returns a subclass instance, the extra fields are not serialised. The response holds what was promised, not what the object happened to carry.
That is a safety property rather than a limitation. It stops a richer inner object leaking through an endpoint documented as returning the base. And it surprises people who expected the subclass's data. SerializeAsAny opts out where the richer output is truly intended and all in it's safe to expose.
The cost
Filtering runs a check pass per response. For a single object it's nothing. For a list of ten thousand it's measurable. It's doing real work. Checking each field of each item against the model.
Two honest options if it ever matters. Return fewer items. This Is often the right answer and is what pagination is for. Or, for a truly hot endpoint, return a Response with pre-serialised content and doc the shape with responses=. Accepting that you've opted out of the checking.
Measure before doing the second. The cost of shipping a field you didn't intend is higher than a few milliseconds.
Response Models: summary
Declare a response model on each endpoint that returns data. It filters the output, validates your own work. Gives the docs a thing to show.
split input and output models. What a caller may send and what they may see are other questions. Use responses= for the failure shapes. And note from_attributes=True when the handler returns an ORM row rather than a dict.
Mistakes people make with Response Models
Not declaring one. The most consequential omission in this module. what the handler returns is sent. A column added to a table next year silently joins the response.
Filtering with args instead of a model. Once you're passing exclude_none plus an exclusion set, a dedicated output model is clearer and can't silently start including a renamed field.
**Losing from_attributes=True.** The usual reason a response model raises on an ORM row when it looks like it should work.
Documenting only the happy path. An endpoint that can 404 should say so with responses=, or a made client has no idea what failure looks like.
Expecting a subclass's extra fields to appear. output follows the declared type, on purpose. It stops a richer inner object leaking through an endpoint documented as returning the base.
Putting secrets in a model and excluding them.Field(exclude=True) works and depends on no one dropping it. A split output model that simply has no such field can't fail that way.
Where to go after Response Models
What happens when the input doesn't fit. The 422. Here It comes from. How to turn it into a thing a caller can act on.
Two directions, two contracts
The symmetry is worth stating plainly.
The request model is a promise to your own code: past check, the data is this shape.
The response model is a promise to everybody else: this is what you'll receive, and nothing more.
Both are enforced. Both appear in the docs. Both are one class each. The discipline they buy. Knowing just what crosses each boundary in each direction. Is most of what splits an API that stays maintainable from one that builds up surprises.
Evolving a response safely
Once clients exist, the response is a contract. The rules for changing it are skewed.
Adding a field is safe. A well-written client skips what it doesn't recognise.
Dropping one is breaking. So is renaming. This Is a removal and an addition.
Changing a type is breaking, including narrowing. A field that was at times null and is now always present will be fine. The reverse won't.
The hands-on sequence for dropping a field: mark it deprecated in the docs, keep returning it, measure whether anyone reads it if you can, then drop it in a new version.
A response model makes all of this visible. This Is the underrated part. Without one, no one can say what the contract was. No one can say whether a change breaks it.
The question to ask each endpoint
For each endpoint that returns data, one question: could this ever hold a thing a caller shouldn't see?
If the answer is no with certainty, a response model is still worth declaring for the docs.
If the answer is anything else. And for anything reading from a DB it often is. The model is the only thing standing between your storage and your consumers. Not a code review, not a rule, not the discipline of whoever writes the next handler. A class that says what may leave.
A closing thought on trust
An API's consumers can't read your handlers. all they know comes from the schema and from what the endpoint really returns. When those two disagree the schema loses. They'll build against the seen action, including the fields you didn't mean to send.
That is the real reason to declare the response. Not tidiness. Not the small check gain. That it makes the documented contract and the actual action the same object. Once they're the same object they can't drift, and a consumer reading your docs is reading the truth rather than an intention.
Questions people ask about Response Models
What does response_model do? It validates the returned data against a model and filters out any fields the model does not declare. It also drives the response schema in the docs.
How do I stop a password field from leaking? Return through an output model that has no password field, set with response_model. Fields not in that model are dropped.
Can I return a different type than the response model? Yes. You can return a dict or an ORM object, and FastAPI converts it to the model. With a return type annotation, FastAPI uses it the same way.
How do I omit fields that were never set? Pass response_model_exclude_unset=True to the decorator. Fields left at their defaults are then left out of the JSON.
What happens if my data doesn't match the model? FastAPI raises a server error, because it is your code that is wrong, not the client. This catches bugs before bad data reaches a client.
Do I document error responses with it too? Yes. The responses argument maps status codes to models, which adds those shapes to the OpenAPI schema.
The cost of not having one
It's worth being concrete about what goes wrong. The failure is never direct.
An endpoint returns a row. Six months later a column is added. An inner flag, a partner's link, an audit field. No one edits the endpoint. No one needs to. The field starts appearing in each response.
If it's harmless, a consumer eventually builds against it and you can no longer drop it. If it isn't harmless, you've been leaking it for but long it takes somebody to notice.
Neither outcome involves anyone making a mistake. That is what makes the response model worth declaring on endpoints that appear not to need one.
Check yourself
0 of 4
Answer without scrolling back up.
What happens without a `response_model`?
The framework sends what you gave it. That is how a column added to a table later silently starts appearing in an API response.
A handler returns `{"id": "abc"}` where the response model declares `id: int`. What does the caller get?
The caller sent nothing wrong; your code broke its own contract, so it is a server error. Without a response model that malformed value would have shipped.
Why use separate Create and Out models?
A single model with everything optional documents nothing - no client can tell what is guaranteed in either direction.
Your handler returns an ORM row and the response model raises. What is usually missing?
By default a model validates from a mapping. `from_attributes=True` - `orm_mode` in Pydantic v1 - lets it read attributes off an object instead.
Cheat sheet
Response Models
Set a response_model on an endpoint, return a full ORM object, and watch FastAPI strip every field the model does not declare before sending.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.