All through this series we obsessed over data coming in: path, query, body, cookies, headers. The last few posts wrapped that up with parameter models. Now we flip it around. What about the data going out? FastAPI can shape, validate, and document your responses too, and one trick here quietly protects you from leaking secrets. 🤔
Declaring a return type
The simplest move: annotate your function's return type, the same way you type a parameter.
from typing import Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class Blog(BaseModel): title: str content: str rating: Optional[float] = None@app.post("/blog")async def create_blog(blog: Blog) -> Blog: return blogLet's review:
The
-> Blogafter the parentheses tells FastAPI the response is aBlog.FastAPI validates the outgoing data against it. If your code returns something that does not fit, you get a server error instead of a wrong response going out the door.
It also documents the response shape in
/docsand serializes it to JSON for you.
Filtering output
Here is the part worth remembering. Say a blog carries an internal field you never want to expose, like the author's email. If you reuse one model for input and output, that field rides along in every response. Bad idea.
The fix is two models plus a response_model.
from typing import Any, Optionalfrom fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class BlogIn(BaseModel): title: str content: str author_email: strclass BlogOut(BaseModel): title: str content: str@app.post("/blog", response_model=BlogOut)async def create_blog(blog: BlogIn) -> Any: return blogLet's review:
BlogInaccepts everything, includingauthor_email.response_model=BlogOutlives on the decorator, not the function. It tells FastAPI to shape the response as aBlogOut.Even though the function returns the full
blog, FastAPI filters the output down to just theBlogOutfields. The email never reaches the client.The return type is
Anyhere so your editor does not complain about returning aBlogInwhere aBlogOutis declared.
So the client sends a body with an email, but the response comes back as just title and content. FastAPI dropped the rest.
A cleaner version with inheritance
That Any return type works, but it loses editor support. A tidier pattern is to make the input model inherit from the output model, then annotate the return type normally.
python
from fastapi import FastAPIfrom pydantic import BaseModelapp = FastAPI()class BaseBlog(BaseModel): title: str content: strclass BlogIn(BaseBlog): author_email: str@app.post("/blog")async def create_blog(blog: BlogIn) -> BaseBlog: return blogLet's review:
BaseBlogholds the public fields.BlogInadds the privateauthor_emailon top.The return type is
BaseBlog, and sinceBlogInis a subclass, your editor is happy returning it.FastAPI still filters the response down to
BaseBlogfields, soauthor_emailis stripped out. Best of both worlds: tooling support and data filtering.
Trimming unset fields
One more handy flag. If your model has defaults but you only want to return the fields that were actually set, add response_model_exclude_unset=True.
python
@app.get("/blog/{blog_id}", response_model=Blog, response_model_exclude_unset=True)async def read_blog(blog_id: int): return {"title": "My First Blog", "content": "Hello FastAPI!"}With this, the optional rating we never set simply will not appear in the response, instead of showing up as null. Cleaner payloads, no clutter.
response_model goes on the decorator (@app.post(...)), not in the function signature like your parameters. Mixing that up is a common early slip. And the filtering is what keeps private fields out of responses, so when in doubt, use a separate output model rather than returning your input model directly. 😁
