Lesson 4 - Response Models
Welcome to Response Models
So far you’ve focused on the data coming in: request bodies, field constraints, and nested models all guard what a client is allowed to send you. But there’s a second contract that’s just as important — the data going out. What your endpoint returns becomes the JSON the client receives, and if you’re not careful, that return value can carry more than you meant to share. A task record might hold a private internal_notes field, an account record might hold a password hash, and the moment your function returns that whole object, it’s on the wire.
FastAPI gives you a single, declarative way to take control of your output: the response model. You tell the path operation, “no matter what my function returns, the response should look like this,” and FastAPI shapes the output to match — keeping the right fields, dropping the rest. In this lesson you’ll use it to make sure your Task Manager never leaks an internal note by accident.
By the end of this lesson, you will be able to:
- Declare a
response_modelon a path operation to describe the output shape - Explain how the response model filters returned data so private fields stay out
- Use the clean pattern of separate input (
TaskIn) and output (TaskOut) models - Apply finer controls like
response_model_excludeandresponse_model_exclude_unset
Let’s take charge of what your API sends back.
Declaring a Response Model
You already know how to declare the input shape: you annotate a function parameter with a Pydantic model and FastAPI validates the request body against it. The response model is the mirror image. It describes the output shape, and you declare it not as a parameter but as an argument on the path operation decorator itself — response_model=.
A path operation is just the decorated function that handles one route, like the function under @app.post("/tasks"). Adding response_model=TaskOut to that decorator tells FastAPI: “whatever this function returns, present it to the client as a TaskOut.”
Here’s a Task Manager endpoint with two separate models — one for what comes in, one for what goes out:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class TaskIn(BaseModel):
title: str
internal_notes: str = "" # private; must NOT be returned
class TaskOut(BaseModel):
id: int
title: str
_store = []
@app.post("/tasks", response_model=TaskOut)
def create(task: TaskIn):
record = {"id": len(_store) + 1, "title": task.title, "internal_notes": task.internal_notes}
_store.append(record)
return record # record HAS internal_notes, but response_model strips itRead the decorator line carefully: @app.post("/tasks", response_model=TaskOut). The "/tasks" is the path, and response_model=TaskOut is a new piece of information — it declares the output contract. The function still receives a validated TaskIn as before, builds a record dictionary, and returns it. But notice the record contains three keys, including the private internal_notes. The response model is about to do something useful with that.
The Response Model Filters Your Output
Here is the key behavior, and it’s worth slowing down for: the response model doesn’t just document the output — it actively filters it. FastAPI takes whatever your function returns, validates it against the response model, and keeps only the fields the response model declares. Anything extra is dropped before the response leaves the server.
Our function returns a record with id, title, and internal_notes. But TaskOut only declares id and title. So internal_notes never makes it into the response. Let’s prove it with FastAPI’s TestClient, which calls the endpoint from code (no running server needed), exactly as in earlier lessons:
from fastapi.testclient import TestClient
client = TestClient(app)
response = client.post("/tasks", json={"title": "Ship it", "internal_notes": "secret plan"})
print(response.json())
print("internal_notes in response:", "internal_notes" in response.json()){'id': 1, 'title': 'Ship it'}
internal_notes in response: FalseLook at what happened. The client sent internal_notes, and our function happily put it into the returned record. Yet the response that came back contains only id and title. The check "internal_notes" in response.json() is False — the private field was filtered out even though the function returned it.
This is the heart of response models. The function’s return value and the client’s response are no longer the same thing. The response model sits between them as a shaping layer: it takes the full object your code produces and trims it down to the declared output contract. That separation is exactly what prevents accidental data leaks — a stray internal field can’t escape, because the response model only lets declared fields through.
The Separate Input/Output Model Pattern
You may have noticed we didn’t reuse one model for both directions — we wrote TaskIn for input and TaskOut for output. That’s a deliberate and very common pattern, and now you can see why it’s so clean.
Input and output rarely have the same shape. Think about creating a task:
- On the way in, the client supplies a
titleand maybeinternal_notes. It does not supply anid— the server assigns that. - On the way out, the client should see the
id(so it can refer to the task later) and thetitle, but not the internal notes.
Trying to force both jobs into a single model gets awkward fast: id would have to be optional so input requests can omit it, and you’d have no clean way to hide internal_notes on the way out. Two focused models sidestep all of that:
class TaskIn(BaseModel):
title: str
internal_notes: str = "" # accepted as input, kept server-side
class TaskOut(BaseModel):
id: int # assigned by the server, shown to the client
title: str # echoed backEach model says exactly one thing. TaskIn is the contract for what a client may send; TaskOut is the contract for what a client will receive. The endpoint bridges them — it accepts a TaskIn, does its work, and FastAPI renders the result as a TaskOut. As your models grow (timestamps, owners, status fields), this split keeps each contract readable and keeps private data firmly on the server side.
The response model is a safety net, not just docs
A response model does three jobs at once. It filters your output so undeclared fields (like internal_notes or a password hash) can never leak. It validates the output, so if your function returns something that can’t fit the declared shape, you find out instead of shipping malformed JSON. And it documents the response: the TaskOut shape appears in the automatic /docs page so clients know exactly what to expect back. Treat your output as a contract every bit as real as your input.
Finer Controls: Excluding Fields
Most of the time, choosing the right response model is all the control you need. But two extra arguments on the decorator let you fine-tune the output without writing a whole new model.
response_model_exclude drops specific named fields from a response model you’d otherwise reuse. Suppose you have a richer TaskOut with a priority field, but for one endpoint you don’t want to send priority:
class TaskOut2(BaseModel):
id: int
title: str
done: bool = False
priority: str = "medium"
@app.post("/tasks-ex", response_model=TaskOut2, response_model_exclude={"priority"})
def create_ex(task: TaskIn):
return {"id": 2, "title": task.title, "done": True, "priority": "high"}{'id': 2, 'title': 'Y', 'done': True}Even though the function returned a priority of "high", the response_model_exclude={"priority"} argument removed it from the response — leaving id, title, and done.
response_model_exclude_unset is different: it leaves out any field that simply fell back to its default and was never explicitly set. This is handy when you don’t want to clutter a response with defaults the caller didn’t actually touch:
@app.post("/tasks-min", response_model=TaskOut2, response_model_exclude_unset=True)
def create_min(task: TaskIn):
return {"id": 1, "title": task.title}{'id': 1, 'title': 'X'}Here the function returned only id and title. Normally TaskOut2 would fill in done and priority from their defaults, but because we set response_model_exclude_unset=True, those untouched defaults are omitted — the response shows only what was actually provided. Reach for these controls when a dedicated model would be overkill; reach for a separate model (like TaskOut) when the difference is structural and permanent.
Practice Exercises
Exercise 1: Where does the leak get stopped?
In the create endpoint, the function returns a record dictionary that includes internal_notes, yet the client never sees it. Where, exactly, is internal_notes removed — inside your function, or somewhere else? What declared the rule that removed it?
Hint
Your function does not remove anything — it returns the full record with all three keys. FastAPI removes internal_notes after your function returns, by validating the output against the response_model=TaskOut. Since TaskOut declares only id and title, every other field is filtered out before the response is sent.
Exercise 2: Why two models instead of one?
Suppose you tried to use a single Task model for both input and output, with fields id, title, and internal_notes. What two problems would that cause for (a) accepting a create request and (b) returning a safe response?
Hint
(a) Clients don’t send an id on create — the server assigns it — so id would have to be made optional, weakening your input contract. (b) internal_notes would be part of the model, so it would be returned to the client unless you added extra exclude rules. Separate TaskIn and TaskOut models solve both cleanly: id is required only on output, and internal_notes exists only on input.
Exercise 3: Predict the filtered output
Given TaskOut2 (fields id, title, done, priority) and the decorator @app.post("/tasks-ex", response_model=TaskOut2, response_model_exclude={"priority"}), the function returns {"id": 2, "title": "Y", "done": True, "priority": "high"}. What JSON does the client receive?
Hint
The client receives {"id": 2, "title": "Y", "done": True}. The response model would normally include priority, but response_model_exclude={"priority"} drops it, so the "high" value never reaches the client.
Summary
A response model declares the shape of what your endpoint sends back. You set it with the response_model= argument on the path operation decorator, and it does more than document — it filters your function’s return value down to only the declared fields, so private data like internal_notes can’t leak even when your code returns it. Because input and output usually differ, the clean, common pattern is two models: a TaskIn for what clients may send and a TaskOut for what they receive. When you need finer control without a whole new model, response_model_exclude drops named fields and response_model_exclude_unset omits fields left at their defaults. The response model also publishes the output shape in /docs.
Key Concepts
- Response model — the
response_model=argument declaring an endpoint’s output shape. - Output filtering — FastAPI keeps only the response model’s fields, dropping any extras the function returned.
- Input/output split — separate
TaskInandTaskOutmodels keep each contract focused and private fields server-side. response_model_exclude— removes specific named fields from the response.response_model_exclude_unset— omits fields that were never explicitly set (left at defaults).
Why This Matters
Data leaks are one of the most common and damaging API mistakes, and they usually happen by accident — a developer returns a whole database record and forgets it carries a password hash or a private note. Response models make safe output the default: you declare what’s allowed out, and FastAPI enforces it on every response, every time. Combined with the input models from earlier lessons, you now control both ends of your API’s contract, which is exactly what production-quality APIs require.
Next Steps
Continue to Lesson 5 - Guided Project: Validated Tasks API
Put request bodies, validation, nested models, and response models together to build a complete, validated Tasks API.
Back to Module Overview
Return to the Request Bodies and Pydantic module overview
Continue Building Your Skills
You now control your API’s output the same way you control its input: declaratively, with a model. That means private fields stay private, responses are documented automatically, and your input and output contracts are clean and separate. In the next lesson you’ll bring the whole module together — request bodies, field constraints, nested models, and response models — into a single guided project: a fully validated Tasks API.