FastAPI Interactive Docs

Response Model - Return Type

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.

main.py
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 blog

Let's review:

  1. The -> Blog after the parentheses tells FastAPI the response is a Blog.

  2. 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.

  3. It also documents the response shape in /docs and 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.

main.py
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 blog

Let's review:

  1. BlogIn accepts everything, including author_email.

  2. response_model=BlogOut lives on the decorator, not the function. It tells FastAPI to shape the response as a BlogOut.

  3. Even though the function returns the full blog, FastAPI filters the output down to just the BlogOut fields. The email never reaches the client.

  4. The return type is Any here so your editor does not complain about returning a BlogIn where a BlogOut is 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 blog

Let's review:

  1. BaseBlog holds the public fields. BlogIn adds the private author_email on top.

  2. The return type is BaseBlog, and since BlogIn is a subclass, your editor is happy returning it.

  3. FastAPI still filters the response down to BaseBlog fields, so author_email is 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. 😁

LAB: