Response models, and the docs page that writes itself#

← Back to FastAPI

Why bother with two models#

create_note above still builds a plain dict, new_note, exactly as it did before response_model entered the picture. response_model=NoteOut doesn't change how the function does its job; it validates and filters whatever the function returns against NoteOut's shape on the way out, and documents that shape for anyone reading /docs. Using one model for both directions can work at first, but it stops making sense the moment a field belongs on one side only, id here is assigned by the server and must never be something a client can set by including it in a POST body, so it has no business appearing on NoteIn at all. This is the same split ../../notes/03-dataclasses.md's tags: list[str] field hinted at for a single attribute; here it's the whole model, split in two.

Reading /docs#

Run any app from this sub-step and open http://127.0.0.1:8000/docs in a browser. Each path operation you've written appears as its own entry: the path, the method, the request body's exact shape if it takes one (pulled straight from the model you passed as a parameter), and the response's exact shape (pulled from response_model, or inferred from the return type if you set one). Click "Try it out" on any entry and you can fill in real values and send an actual request, right there, and see the real response, including a 422 on a deliberately broken request. Nothing about that page was hand-written; every field of it traces back to a decorator, a type hint, or a pydantic model already in your source file.

What to watch for#

  • Reusing one model for both request and response out of laziness. It often works at first, until a field like id needs to exist on the way out but never on the way in.
  • Expecting response_model to change what your function returns. It doesn't; your function still builds and returns the data itself. response_model validates and filters that return value against the model's shape (and documents it), it isn't a second copy of your logic.
  • Forgetting /docs exists at all, and manually re-testing every endpoint with curl the way Week 2's testing section had you do. /docs replaces most of that loop during development, and it stays accurate automatically as your models change, since it's reading the same code you're already editing.

TL;DR#

The same type hints driving request validation also describe what a path operation sends back, and FastAPI turns every hint you've written across the whole app into one live, interactive documentation page, generated, not authored.

  • response_model=NoteOut on a path operation's decorator declares the shape of a correct response, the same role a request body model plays for incoming data, just pointed the other direction.
  • A separate output model is a normal, deliberate choice. NoteOut can include a server-assigned id field that NoteIn never accepts from the client, because the two models describe two different moments, what comes in and what goes out, not the same data twice.
  • Visit /docs on a running app and every path, method, request body model, and response model you've declared is already there: an interactive page (Swagger UI) listing each endpoint, its expected input, its possible responses, and a "Try it out" button that sends a real request without curl.
  • None of /docs is separately written. It's generated by reading the exact same FastAPI() app, decorators, and pydantic models already sitting in your code, a second time, for documentation instead of validation.
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class NoteIn(BaseModel):
    text: str


class NoteOut(BaseModel):
    id: int
    text: str


@app.post("/notes", response_model=NoteOut)
def create_note(note: NoteIn):
    new_note = {"id": next_id(), "text": note.text}
    notes.append(new_note)
    return new_note