Automatic Docs and OpenAPI
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Other people must know how to call your API. FastAPI writes that description for you, in a standard format, from the code you already wrote. You can read it in Python, in a browser, or with other tools.
OpenAPI is a description of your API
OpenAPI is a standard JSON format that describes an HTTP API: paths, methods, parameters, request bodies and responses. FastAPI generates it from route functions and models.
OpenAPI is a standard way to describe an API as JSON: its paths, methods, parameters, bodies and answers. FastAPI builds this description from your code. You do not write it by hand, so it can never drift away from the real routes.
schema = app.openapi()print(schema["info"]["title"])print(list(schema["paths"]))Example 1 · Read the generated schema
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClientfrom pydantic import BaseModel, Field
app = FastAPI(title="Shop API", version="1.2.0", description="Products for the shop.")
class ProductIn(BaseModel): name: str = Field(description="Name shown to customers", min_length=2) price: float = Field(gt=0, examples=[49.5])
class ProductOut(ProductIn): id: int
@app.post("/products", response_model=ProductOut, status_code=201, tags=["products"], summary="Add a product")def add_product(product: ProductIn): """Create a product and return it with its new id.""" return {"id": 1, **product.model_dump()}
@app.get("/products/{product_id}", response_model=ProductOut, tags=["products"])def read_product(product_id: int, include_stock: bool = False): raise HTTPException(status_code=404, detail="Not found")
schema = app.openapi()print(schema["info"]["title"], schema["info"]["version"])for path, methods in schema["paths"].items(): for method in methods: print(method.upper(), path)Shop API 1.2.0
POST /products
GET /products/{product_id}What this code does
app.openapi()returns the schema as a normal dict, so you can print parts of it.- The version and title come from the
FastAPI(...)call.
The paths key lists every route. Each path holds its methods, and each method holds its parameters and answers.
Write the docs in the code you already have
Type hints, models and status codes already feed the schema. Add summary, tags and a docstring on a route to make the page easier to read. Add description and examples inside Field to explain a model field.
@app.post( "/products", response_model=ProductOut, status_code=201, tags=["products"], summary="Add a product",)def add_product(product: ProductIn): """Create a product and return it."""Example 2 · One operation in detail
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClientfrom pydantic import BaseModel, Field
app = FastAPI(title="Shop API", version="1.2.0", description="Products for the shop.")
class ProductIn(BaseModel): name: str = Field(description="Name shown to customers", min_length=2) price: float = Field(gt=0, examples=[49.5])
class ProductOut(ProductIn): id: int
@app.post("/products", response_model=ProductOut, status_code=201, tags=["products"], summary="Add a product")def add_product(product: ProductIn): """Create a product and return it with its new id.""" return {"id": 1, **product.model_dump()}
@app.get("/products/{product_id}", response_model=ProductOut, tags=["products"])def read_product(product_id: int, include_stock: bool = False): raise HTTPException(status_code=404, detail="Not found")
schema = app.openapi()post = schema["paths"]["/products"]["post"]print(post["summary"], post["tags"])print(post["description"])print(sorted(post["responses"]))get = schema["paths"]["/products/{product_id}"]["get"]for param in get["parameters"]: print(param["name"], param["in"], param["required"])Add a product ['products'] Create a product and return it with its new id. ['201', '422'] product_id path True include_stock query False
What this code does
- The summary and tags come from the decorator and the description comes from the docstring.
- Parameters show where each value lives (
pathorquery) and whether it is required.
The docstring becomes the long description. Tags group routes together on the docs page.
Models become schemas
Every Pydantic model used in a route appears under components and schemas. A route points to it with a $ref. The schema lists the properties, which fields are required, and every limit you set with Field.
schemas = app.openapi()["components"]["schemas"]print(schemas["ProductIn"]["required"])print(schemas["ProductIn"]["properties"]["price"])Example 3 · A model becomes a schema
from fastapi import FastAPI, HTTPExceptionfrom fastapi.testclient import TestClientfrom pydantic import BaseModel, Field
app = FastAPI(title="Shop API", version="1.2.0", description="Products for the shop.")
class ProductIn(BaseModel): name: str = Field(description="Name shown to customers", min_length=2) price: float = Field(gt=0, examples=[49.5])
class ProductOut(ProductIn): id: int
@app.post("/products", response_model=ProductOut, status_code=201, tags=["products"], summary="Add a product")def add_product(product: ProductIn): """Create a product and return it with its new id.""" return {"id": 1, **product.model_dump()}
@app.get("/products/{product_id}", response_model=ProductOut, tags=["products"])def read_product(product_id: int, include_stock: bool = False): raise HTTPException(status_code=404, detail="Not found")
schemas = app.openapi()["components"]["schemas"]print(sorted(schemas))product = schemas["ProductIn"]print(product["required"])print(product["properties"]["name"]["description"])print(product["properties"]["name"]["minLength"])print(product["properties"]["price"]["exclusiveMinimum"], product["properties"]["price"]["examples"])['HTTPValidationError', 'ProductIn', 'ProductOut', 'ValidationError'] ['name', 'price'] Name shown to customers 2 0.0 [49.5]
What this code does
ProductOutinherits fromProductIn, so both appear under schemas.- The
Fieldlimits are copied into the schema:minLength,exclusiveMinimumand the examples.
Other tools can read this schema to create client code, mock servers and tests. That is why a clear schema is part of the API, not decoration.
The docs pages and switching them off
Swagger UI is the web page at /docs that lists your routes and lets you try them from the browser. FastAPI creates it from the OpenAPI schema.
FastAPI serves the schema at /openapi.json, an interactive page at /docs (Swagger UI) and a reading page at /redoc. A public API often keeps them. A private one can switch them off with docs_url=None, or hide a single route with include_in_schema=False.
app = FastAPI(docs_url=None, redoc_url=None)
@app.get("/health", include_in_schema=False)def health(): return {"ok": True}Example 4 · Docs pages as seen by a test
from fastapi import FastAPIfrom fastapi.testclient import TestClient
app = FastAPI(title="Shop API")
@app.get("/health", include_in_schema=False)def health(): return {"ok": True}
@app.get("/items")def items(): return []
client = TestClient(app)print(client.get("/docs").status_code)print("swagger" in client.get("/docs").text.lower())print(client.get("/redoc").status_code)print(list(client.get("/openapi.json").json()["paths"]))print(client.get("/health").json())
private = FastAPI(docs_url=None, redoc_url=None)print(TestClient(private).get("/docs").status_code)200
True
200
['/items']
{'ok': True}
404What this code does
- A test can check that the docs page exists without opening a browser.
/healthworks but is not listed. A private app answers 404 on/docs.
Example 5 · A contract test: every route has a summary
from fastapi import FastAPI
app = FastAPI()
@app.get("/orders", summary="List orders")def orders(): return []
@app.get("/customers")def customers(): return []
@app.get("/tickets", summary="List tickets")def tickets(): return []
missing = [r.path for r in app.routes if hasattr(r, "summary") and not r.summary]print(missing)print(app.openapi()["paths"]["/customers"]["get"]["summary"])['/customers'] Customers
What this code does
- The route objects keep the summary you wrote, or
None. This test finds routes that have no hand-written summary. - The schema still shows a default summary made from the function name.
The route still works. It is only missing from the schema and the docs page.
Next step
Check your understanding
12 quick questions