Free lesson

Configure OpenAPI documentation with examples

You will enrich the auto-generated OpenAPI documentation for production use. Add response_model_exclude_unset=True for clean JSON responses. Define multiple response examples using OpenAPI's example syntax in Pydantic model Config. Add tags and descriptions to group endpoints logically. Configure the OpenAPI schema with servers, contact info, and license. Build a custom Swagger UI page with try-it-out enabled and a Redoc alternative.

~25 min read · Free to read — no subscription required.

Configure OpenAPI documentation with examples

Introduction

When you hand a FastAPI service to a frontend team, an SDK generator, or an LLM tool-use planner, the only contract they read is /openapi.json. If that schema shows "string" placeholders instead of realistic payloads, no tags, and no documented error shapes, integrators guess — and guesses become production bugs. FastAPI emits OpenAPI 3.1 from your code automatically, but the defaults stop at "technically correct"; the gap from there to useful is the metadata, examples, and overrides you attach. By the end of this lesson you will be able to configure application-level info metadata, group routes with tags, attach realistic request and response examples, document non-2xx responses, hide internal endpoints, and replace the schema generator to inject servers and security schemes.

Key Terminology

  • OpenAPI schema — the JSON document FastAPI builds from your routes, type hints, and Pydantic models and serves at /openapi.json; everything in this lesson is about shaping it.
  • Path operation metadata — keyword arguments on @app.get/@app.post (summary, description, response_description, responses, tags, include_in_schema) that decorate a single endpoint in the generated schema.
  • Example — a sample value attached via Field(examples=[...]), Body(examples={...}), or model_config.json_schema_extra, rendered by Swagger UI as the default payload when a user clicks "Try it out".
  • responses dict — a {status_code: {"model": ..., "description": ...}} mapping on a path operation that documents 4xx/5xx shapes alongside the success response.
  • custom_openapi hook — a function assigned to app.openapi that mutates the generated schema (adding info.contact, servers, securitySchemes, x- extensions) and caches the result on app.openapi_schema.

Concepts

FastAPI builds the OpenAPI document lazily on the first request to /openapi.json. It walks every registered route, reads decorator metadata (path, method, status code, tags, summary, description, responses, include_in_schema), introspects the handler signature for parameters and request body, and resolves Pydantic models into JSON Schema entries under components.schemas. Examples declared on Field, Body, or model_config.json_schema_extra are merged into the schema. The result is cached on app.openapi_schema, so subsequent requests are cheap.

Loading diagram...

Every piece of metadata flows through this single pipeline, so the configuration surface is small and predictable.

Where metadata attaches

Four levels, narrowest scope wins:

  • ApplicationFastAPI(title=..., version=..., description=..., contact=..., license_info=...) populates the OpenAPI info object shown in the Swagger UI header and consumed by SDK generators.
  • RouterAPIRouter(prefix=..., tags=[...]) groups every route on the router into the same collapsible section in Swagger UI; tags can also be passed at include_router time.
  • Path operationsummary, description (markdown), response_description, and responses decorate a single endpoint; include_in_schema=False hides it entirely.
  • Model / fieldField(examples=[...]) puts a value next to one field; model_config = ConfigDict(json_schema_extra={"examples": [...]}) puts a full-record example on the model. Body(..., examples={"minimal": {...}, "full": {...}}) attaches multiple named examples to a request body, which Swagger UI renders as a dropdown (see Code Walkthrough).

Overriding the generator

For anything the decorators cannot express — info.contact, multiple servers, securitySchemes, x- extensions, versioned docs — assign a function to app.openapi. The function should short-circuit on app.openapi_schema (cache), call get_openapi(...) for the default build, mutate, then store and return. Forgetting the cache guard rebuilds the schema on every request.

Code Walkthrough

The two snippets below demonstrate the four metadata levels and the override hook from the previous section. The first attaches application, router, operation, model, and field metadata for a documented GET; the second adds a Body(..., examples={...}) request example, hides an internal endpoint, and swaps in a custom_openapi hook that injects servers and bearer auth.

Code snippetpython
1from fastapi import APIRouter, FastAPI, HTTPException, status 2from pydantic import BaseModel, ConfigDict, Field 3 4app = FastAPI( 5 title="Prompt Management API", 6 version="1.2.0", 7 description="Internal service for storing and retrieving GenAI prompt templates.", 8 contact={"name": "Platform Team", "email": "platform@example.com"}, 9) 10 11class ErrorResponse(BaseModel): 12 detail: str = Field(..., examples=["Prompt 'prompt_0042' not found"]) 13 14class PromptOut(BaseModel): 15 prompt_id: str = Field(..., examples=["prompt_0042"]) 16 name: str = Field(..., examples=["summarise-ticket"]) 17 template: str = Field(..., examples=["Summarise the following ticket: {ticket}"]) 18 19 model_config = ConfigDict( 20 json_schema_extra={ 21 "examples": [{ 22 "prompt_id": "prompt_0042", 23 "name": "summarise-ticket", 24 "template": "Summarise the following ticket: {ticket}", 25 }] 26 } 27 ) 28 29router = APIRouter(prefix="/prompts", tags=["prompts"]) 30 31@router.get( 32 "/{prompt_id}", 33 response_model=PromptOut, 34 summary="Fetch a prompt by ID", 35 description="Returns the **full prompt record** including template body and metadata.", 36 response_description="The matching prompt record", 37 responses={404: {"model": ErrorResponse, "description": "Prompt not found"}}, 38) 39def get_prompt(prompt_id: str) -> PromptOut: 40 raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Prompt '{prompt_id}' not found") 41 42app.include_router(router)

FastAPI(...) populates the info object. APIRouter(tags=["prompts"]) groups the routes. The decorator carries summary, description, response_description, and responses so Swagger UI renders both the 200 and 404 schemas. Field(examples=[...]) puts realistic values on each property; model_config = ConfigDict(json_schema_extra={"examples": [...]}) adds a full-record example used in the response preview pane.

Code snippetpython
1from fastapi import Body, FastAPI 2from fastapi.openapi.utils import get_openapi 3from pydantic import BaseModel 4 5class PromptIn(BaseModel): 6 name: str 7 template: str 8 9@app.post("/prompts") 10def create_prompt( 11 payload: PromptIn = Body( 12 ..., 13 examples={ 14 "minimal": {"summary": "Smallest valid payload", 15 "value": {"name": "echo", "template": "Echo: {input}"}}, 16 "full": {"summary": "Realistic production prompt", 17 "value": {"name": "summarise-ticket", 18 "template": "Summarise: {ticket}"}}, 19 }, 20 ), 21): 22 return {"prompt_id": "prompt_0001", **payload.model_dump()} 23 24@app.get("/_internal/healthz", include_in_schema=False) 25def healthz(): 26 return {"status": "ok"} 27 28def custom_openapi(): 29 if app.openapi_schema: 30 return app.openapi_schema 31 schema = get_openapi(title=app.title, version=app.version, routes=app.routes) 32 schema["servers"] = [ 33 {"url": "https://api.example.com/v1", "description": "Production"}, 34 {"url": "https://staging.example.com/v1", "description": "Staging"}, 35 ] 36 schema["components"].setdefault("securitySchemes", {})["bearerAuth"] = { 37 "type": "http", "scheme": "bearer", "bearerFormat": "JWT", 38 } 39 schema["security"] = [{"bearerAuth": []}] 40 app.openapi_schema = schema 41 return schema 42 43app.openapi = custom_openapi

Body(..., examples={...}) produces a dropdown in Swagger UI so callers pick minimal or full before hitting "Try it out". include_in_schema=False keeps /healthz reachable but invisible in the schema. custom_openapi short-circuits on the cache, calls get_openapi(...), then injects servers and a bearerAuth scheme that surfaces as an "Authorize" button in /docs.

You'll know it works when curl /openapi.json | jq '.info.contact, .components.securitySchemes, .paths["/prompts"].get' shows your contact block, the bearerAuth scheme, and the prompts tag with the 404 response documented — and /docs renders the example dropdown on POST /prompts without /_internal/healthz appearing anywhere.

Do's and Don'ts

Building on the configuration patterns above, the rules below distil what to repeat and what to avoid when shaping /openapi.json for real consumers.

Do's

  1. Do attach responses={404: {"model": ErrorResponse}, ...} to every operation that raises — without it, clients and SDK generators see no 4xx shape and integrators guess the error contract.
  2. Do cache inside custom_openapi with the if app.openapi_schema: return ... guard — rebuilding the schema on every /openapi.json request dominates p95 latency on docs-heavy traffic.
  3. Do prefer Body(..., examples={...}) with named entries over a single example — the dropdown shows minimal, full, and edge-case payloads so callers learn the shape without reading prose.

Don'ts

  1. Don't treat include_in_schema=False as authentication — the route still serves traffic; pair it with an auth dependency or a network policy.
  2. Don't declare the same tag on both APIRouter and the path operation — tags merge, producing duplicate sections in Swagger UI; pick one level.
  3. Don't omit response_model — without it FastAPI infers the response from the return annotation but skips output filtering, and Swagger UI shows an anonymous schema instead of a named reference.

Keep going with GenAI Agent Engineering

Create a free account to track your progress and open this lesson in the full learning view. Subscribe to unlock the entire path — every goal, the hands-on labs, quizzes, and your verifiable skill graph — from . Cancel anytime.