Customizing OpenAPI

FastAPI · Advanced · 12 min

A precise schema is what client SDKs, API gateways and partner teams are built on, so it deserves the same care as code.

The generated schema is good by default; a few knobs make it excellent.

Extra responses. FastAPI only knows the success response and the 422. Document the rest with responses={404: {"model": Message, "description": "Item not found"}} on a decorator or router, so clients know the error shapes.

Examples. Field(examples=[...]) on model fields, or model_config = {"json_schema_extra": {"examples": [...]}} for a whole model, appear in the JSON Schema. Body(openapi_examples={...}) adds named examples that Swagger UI shows in a dropdown.

Organization. Tag every operation, and describe tags with FastAPI(openapi_tags=[{"name": "items", "description": "..."}]), which also fixes their order. Hide internal endpoints with include_in_schema=False.

Operation ids. Client generators turn each operationId into a method name. The defaults are long (read_item_items__item_id__get), so pass generate_unique_id_function to FastAPI() to build names like items-read_item from the route's tag and function name. They must stay unique.

Input vs output schemas. With Pydantic v2, a model with defaults can produce separate Item-Input and Item-Output schemas, because output fields with defaults are always present. That's accurate; if your tooling dislikes it, FastAPI(separate_input_output_schemas=False) merges them.

Post-processing. For anything else, replace app.openapi with a function that calls fastapi.openapi.utils.get_openapi(...), edits the dict (add a logo, security schemes, vendor extensions) and caches it in app.openapi_schema, so it's built once rather than on every request.

Example

from fastapi import FastAPI
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

def op_id(route: APIRoute) -> str:
    tag = route.tags[0] if route.tags else "default"   # untagged routes too
    return f"{tag}-{route.name}"

app = FastAPI(generate_unique_id_function=op_id,
              openapi_tags=[{"name": "items", "description": "Warehouse items"}])

class Item(BaseModel):
    name: str = Field(examples=["Fountain pen"])
    price: float = Field(gt=0, examples=[12.5])

class Message(BaseModel):
    detail: str

@app.get("/items/{item_id}", tags=["items"], responses={404: {"model": Message}})
def read_item(item_id: int) -> Item:
    return Item(name="Pen", price=2)

@app.get("/internal/metrics", include_in_schema=False)
def metrics():
    return {}

spec = TestClient(app).get("/openapi.json").json()
op = spec["paths"]["/items/{item_id}"]["get"]
print(op["operationId"], sorted(op["responses"]))   # items-read_item ['200', '404', '422']
print("/internal/metrics" in spec["paths"])          # False

Remember

  • responses={code: {...}} documents error shapes beyond 200 and 422
  • Field examples and Body openapi_examples improve the docs
  • generate_unique_id_function gives client SDKs clean method names
  • Override app.openapi and cache in app.openapi_schema for anything else

Watch out for

  • Overriding app.openapi without caching the result, so the schema is regenerated on every request.

Check yourself

  1. Generated TypeScript client methods have names like read_item_items__item_id__get. What fixes this at the source?
  2. Why might one model appear as Item-Input and Item-Output in the schema?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: fastapi.tiangolo.com/how-to/extending-openapi/

Customizing OpenAPI · FastAPI · Topolearn