Response Models

Declaring what comes back - which documents the endpoint and, more usefully, decides what cannot leak.

Overview

The default is generous

Without a response model, FastAPI serialises whatever your handler returns.

That is convenient and it is how data leaks. A handler that returns a database row returns *all* of it: internal notes, an email address, a password hash, a flag nobody outside should know exists. Nothing warns you, because from the framework's point of view you asked for exactly 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, because 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 documents 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
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
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
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
Output

Dropping unset and null fields

response_model_exclude_none and its siblings trim the output without a second model.

example_05.pyFastAPI
Output

Input and output are different shapes

Separate models for what you accept and what you return — the pattern almost every real endpoint ends up with.

example_06.pyFastAPI
Output

Declaring the shape

@app.get("/modules/1", response_model=ModuleOut)
def read():
    return ROW

Now the return value is passed through ModuleOut. Fields the model declares are kept; everything else is dropped.

The handler is unchanged — it still returns the whole row — but the model decides what leaves. That separation is the useful part: your data access can return whatever is natural, and the contract with the outside world is stated once, in a class, where it can be reviewed.

The modern spelling is a return annotation:

def list_modules() -> List[ModuleOut]:

Same behaviour, and it reads as Python rather than as framework configuration. Use response_model= when the two need to differ — returning a Response directly, for instance, while still documenting the shape.

It checks your work

The response is validated against the model, which 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 produced something that does not match its own contract, and the framework refuses to ship it.

That is the right behaviour 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 arguments handle the common adjustments without a second model.

response_model_exclude_none=True drops fields that are None. Useful for a sparse response where nulls carry no information.

response_model_exclude_unset=True drops fields that were never set, keeping explicit nulls. This is the one that matters for anything update-shaped, for the reasons the Pydantic track laid out.

response_model_exclude={"field"} removes named fields.

They are convenient and they are a slope. Once you are passing two of them plus an exclusion set, a dedicated output model is clearer, appears correctly in the schema, and cannot silently start including a field that was renamed.

Input and output are different questions

The pattern almost every endpoint converges on is separate models per direction.

Create takes what a caller may supply. No id, because 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 internal, and is required to be complete — by the time you are returning one, those fields exist.

Update has everything 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, and it produces an API that documents nothing: every field might be missing, so no client can tell what is guaranteed either way.

What it does for the documentation

The response model is what makes the generated docs describe the response, not just the request.

Without one, the OpenAPI document says the endpoint returns "anything". Every generated client then produces an untyped result, and every consumer writes their own guess at the shape.

With one, the schema describes the response precisely, and clients get a real type. That is the difference between an API somebody can build against confidently and one they have to explore by trial.

Descriptions and examples on output fields are worth writing for the same reason they are on inputs — they appear in the docs next to the field.

A few practical notes

Status codes. response_model describes the success response. Error shapes are documented separately with responses={404: {...}}, which is worth doing for an API with consumers.

Lists. -> List[ModuleOut] filters each item. There is no extra work for collections.

ORM objects. If the handler returns an ORM row rather than a dict, the output model needs from_attributes=True so it can read attributes. That is the setting called orm_mode in Pydantic v1, and it is the usual reason a response model raises when it looks like it should work.

Performance. Filtering costs a validation pass per response. It is cheap, and for a large list it is not free — but shipping fields you did not intend to is a worse problem than a few microseconds.

The habit worth forming

Declare a response model on every endpoint that returns data, even when it looks identical to what you are returning anyway.

The value is not in today's filtering; it is that the endpoint now has a written contract. When a column is added to the underlying table next year, the response does not change, because something declared what the response is.

Documenting the errors too

response_model describes the success case. An endpoint that can return a 404 says nothing about it by default, so a generated 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 every endpoint that can fail in a meaningful way. It costs a line and it turns "you will get something on failure" into a documented contract.

Status codes and the response model

The declared model describes the route's *default* status. A handler returning a different status with a different shape — a 202 with a job id rather than a 201 with the object — is returning something the schema does not describe.

responses= can document those alternatives with their own models, which keeps the document honest. The alternative, an endpoint whose real behaviour is broader than its schema, is the thing that makes generated 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 contains what was promised, not what the object happened to carry.

That is a safety property rather than a limitation — it stops a richer internal 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 genuinely intended and everything in it is safe to expose.

The cost

Filtering runs a validation pass per response. For a single object it is nothing. For a list of ten thousand it is measurable, and it is doing real work — checking every field of every item against the model.

Two honest options if it ever matters. Return fewer items, which is usually the right answer and is what pagination is for. Or, for a genuinely hot endpoint, return a Response with pre-serialised content and document the shape with responses= — accepting that you have opted out of the checking.

Measure before doing the second. The cost of shipping a field you did not intend is higher than a few milliseconds.

Summary

Declare a response model on every endpoint that returns data. It filters the output, validates your own work, and gives the documentation something to describe.

Separate input and output models, because what a caller may send and what they may see are different questions. Use responses= for the failure shapes. And remember from_attributes=True when the handler returns an ORM row rather than a dict.

Mistakes people make

Not declaring one. The most consequential omission in this module. Whatever the handler returns is sent, and a column added to a table next year silently joins the response.

Filtering with arguments instead of a model. Once you are passing exclude_none plus an exclusion set, a dedicated output model is clearer and cannot silently start including a renamed field.

Forgetting 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 generated client has no idea what failure looks like.

Expecting a subclass's extra fields to appear. Serialisation follows the declared type, deliberately — it stops a richer internal 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 nobody removing it. A separate output model that simply has no such field cannot fail that way.

Next

What happens when the input does not fit: the 422, where it comes from, and how to turn it into something 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 validation, the data is this shape.

The response model is a promise to everybody else: this is what you will receive, and nothing more.

Both are enforced. Both appear in the documentation. Both are one class each, and the discipline they buy — knowing exactly what crosses each boundary in each direction — is most of what separates an API that stays maintainable from one that accumulates surprises.

Evolving a response safely

Once clients exist, the response is a contract, and the rules for changing it are asymmetric.

Adding a field is safe. A well-written client ignores what it does not recognise.

Removing one is breaking. So is renaming, which is a removal and an addition.

Changing a type is breaking, including narrowing — a field that was sometimes null and is now always present will be fine, but the reverse will not.

The practical sequence for removing a field: mark it deprecated in the docs, keep returning it, measure whether anyone reads it if you can, then remove it in a new version.

A response model makes all of this visible, which is the underrated part. Without one, nobody can say what the contract was, so nobody can say whether a change breaks it.

The question to ask each endpoint

For every endpoint that returns data, one question: could this ever contain something a caller should not see?

If the answer is no with certainty, a response model is still worth declaring for the documentation.

If the answer is anything else — and for anything reading from a database it usually is — the model is the only thing standing between your storage and your consumers. Not a code review, not a convention, 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 cannot read your handlers. Everything they know comes from the schema and from what the endpoint actually returns, and when those two disagree the schema loses — they will build against the observed behaviour, including the fields you did not mean to send.

That is the real reason to declare the response. Not tidiness, and not the small validation benefit, but that it makes the documented contract and the actual behaviour the same object. Once they are the same object they cannot drift, and a consumer reading your docs is reading the truth rather than an intention.

The cost of not having one

It is worth being concrete about what goes wrong, because the failure is never immediate.

An endpoint returns a row. Six months later a column is added — an internal flag, a partner's reference, an audit field. Nobody edits the endpoint, because nobody needs to. The field starts appearing in every response.

If it is harmless, a consumer eventually builds against it and you can no longer remove it. If it is not harmless, you have been leaking it for however 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.

  1. What happens without a `response_model`?

  2. A handler returns `{"id": "abc"}` where the response model declares `id: int`. What does the caller get?

  3. Why use separate Create and Out models?

  4. Your handler returns an ORM row and the response model raises. What is usually missing?

Cheat sheet

Response Models

That is convenient and it is how data leaks. A handler that returns a database row returns *all* of it: internal notes, an email address, a password hash, a flag nobody outside should know exists. Nothing warns you, because from the framework's point of view you asked for exactly that.

FASTAPI · vizlearn.in/fastapi/response_models.html

About the author

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.