Topic 17 of 22
OpenAPI & Documentation
FastAPI automatically generates a full OpenAPI 3.x spec from your code. Learn to control schema generation, enrich metadata, customise Swagger UI, add security schemes, and brand ReDoc — turning your API into living, interactive documentation.
Your FastAPI Code
│ (routes, models, Depends)
▼
┌─────────────────────────┐
│ OpenAPI Schema JSON │ ← /openapi.json
└────────────┬────────────┘
│
┌───────┴────────┐
▼ ▼
Swagger UI ReDoc
/docs /redoc
(interactive) (readable)
17.1
OpenAPI
Schema Generation
▼
FastAPI auto-generates an OpenAPI schema by inspecting your route definitions, path/query parameters, request bodies (Pydantic models), and response models. You get this for free — no extra code needed.
The schema is served as JSON at
/openapi.json. Every route decorator, type hint, and Pydantic field contributes to it automatically.
python — what generates the schema
from fastapi import FastAPI from pydantic import BaseModel from typing import Optional app = FastAPI() class Item(BaseModel): name: str # → required string field in schema price: float # → required number field description: Optional[str] = None # → optional string # FastAPI reads this entire route and builds schema entries: @app.post( "/items/{item_id}", response_model=Item, # → response schema status_code=201, summary="Create an item", # → shows in Swagger UI description="Create a new item with all the info", tags=["items"], # → groups endpoints in UI deprecated=False, ) async def create_item( item_id: int, # → path param in schema item: Item, # → request body schema ): return item
The generated
/openapi.json looks like:json — generated openapi schema (excerpt)
{
"openapi": "3.1.0",
"info": { "title": "My API", "version": "0.1.0" },
"paths": {
"/items/{item_id}": {
"post": {
"summary": "Create an item",
"tags": ["items"],
"parameters": [
{ "name": "item_id", "in": "path", "required": true,
"schema": { "type": "integer" } }
],
"requestBody": {
"content": { "application/json": {
"schema": { "$ref": "#/components/schemas/Item" }
}}
},
"responses": { "201": { "description": "Successful Response" }}
}
}
},
"components": {
"schemas": {
"Item": {
"type": "object",
"required": ["name", "price"],
"properties": {
"name": { "type": "string" },
"price": { "type": "number" },
"description": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
}
}
}
}
}
You can enrich the schema with Field annotations in your Pydantic models — they flow directly into the schema:
python — enriching schema with Field
from pydantic import BaseModel, Field class Item(BaseModel): name: str = Field( ..., title="Item Name", description="The name of the item (must be unique)", min_length=1, max_length=100, examples=["Laptop", "Phone"], ) price: float = Field( ..., gt=0, description="Price in USD, must be greater than 0", examples=[29.99], ) tags: list[str] = Field( default=[], description="List of category tags", ) class Config: # Adds example to the schema's "example" block json_schema_extra = { "example": { "name": "Laptop", "price": 999.99, "tags": ["electronics", "computers"], } }
You can also disable the OpenAPI docs entirely in production with
FastAPI(openapi_url=None). Or change the URL: FastAPI(openapi_url="/api/v1/schema.json").Marking an endpoint as deprecated:
python — deprecated endpoint
@app.get("/old-endpoint", deprecated=True) async def old_route(): """This endpoint is deprecated. Use /new-endpoint instead.""" return {"message": "use /new-endpoint"}
Metadata
▼
Metadata is info about your API itself — title, version, description, contact, license, and tag descriptions. It appears at the top of Swagger UI and is useful for consumers of your API.
python — full metadata setup
from fastapi import FastAPI # Describe your tags — these appear as sections in Swagger UI tags_metadata = [ { "name": "items", "description": "Operations with items. The **magic** happens here.", }, { "name": "users", "description": "Manage users and authentication.", "externalDocs": { "description": "Auth docs", "url": "https://docs.myapi.com/auth", }, }, ] app = FastAPI( title="My Awesome API", summary="A powerful REST API for managing items and users", description=""" ## Welcome to My Awesome API You can: * **Create items** — add new products * **List items** — browse the catalog * **Manage users** — register and authenticate > Markdown is fully supported in the description! """, version="2.1.0", terms_of_service="https://myapi.com/terms", contact={ "name": "API Support", "url": "https://myapi.com/support", "email": "support@myapi.com", }, license_info={ "name": "Apache 2.0", "identifier": "Apache-2.0", }, openapi_tags=tags_metadata, )
The docstring of your route function also becomes the endpoint description (supports Markdown):
python — docstring as endpoint description
@app.get("/items/{item_id}", tags=["items"]) async def get_item(item_id: int): """ Get a single item by ID. - **item_id**: The unique identifier of the item - Returns full item details including description and price - Raises 404 if item not found """ return {"item_id": item_id}
The
summary is shown inline in the endpoint list (one line). The description / docstring is shown when you expand an endpoint. Use both for the best docs experience.You can also exclude endpoints from the schema using
include_in_schema=False — useful for internal routes:python — hide endpoint from docs
@app.get("/internal/health", include_in_schema=False) async def internal_health(): # This endpoint works but is invisible in /docs and /openapi.json return {"status": "ok"}
17.2
Swagger UI
Swagger UI is the interactive documentation auto-served at
/docs. It lets you explore and test every endpoint directly from the browser — no Postman needed.
items
Operations with items. The magic happens here.
GET
/items
List all items
POST
/items/{item_id}
Create an item
PUT
/items/{item_id}
Update an item
DELETE
/items/{item_id}
Delete an item
usersManage users and authentication.
POST
/users/register
Register a user
Customization
▼
You can fully replace the default Swagger UI by mounting your own
/docs route. This lets you control the JS/CSS versions, layout, and any Swagger config options.
python — custom swagger UI route
from fastapi import FastAPI from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import HTMLResponse # Disable default docs first app = FastAPI(docs_url=None, redoc_url=None) @app.get("/docs", include_in_schema=False) async def custom_swagger_ui() -> HTMLResponse: return get_swagger_ui_html( openapi_url="/openapi.json", title="My API — Documentation", swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", swagger_favicon_url="/static/favicon.png", swagger_ui_parameters={ "defaultModelsExpandDepth": -1, # hide schemas section by default "docExpansion": "list", # "none" | "list" | "full" "filter": True, # adds search filter box "syntaxHighlight.theme": "monokai", "tryItOutEnabled": True, # auto-enable Try it out }, )
Use local CDN assets if you want Swagger UI to work offline or in an air-gapped environment. Download the dist files and serve them via
StaticFiles.Changing docs URLs — simple version:
python — changing docs / redoc urls
# Move docs to /api/docs, disable redoc app = FastAPI( docs_url="/api/docs", redoc_url=None, # disable ReDoc openapi_url="/api/schema", # custom schema URL ) # Or disable ALL docs in production: import os app = FastAPI( docs_url=None if os.getenv("APP_ENV") == "production" else "/docs", redoc_url=None if os.getenv("APP_ENV") == "production" else "/redoc", )
Security Integration
▼
You can add an Authorize 🔒 button to Swagger UI by declaring security schemes in the OpenAPI schema. This lets users enter their JWT token or API key once and have it sent with every "Try it out" request.
python — bearer token in swagger ui
from fastapi import FastAPI, Depends, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials app = FastAPI() # Declare security scheme — this adds the Authorize button security = HTTPBearer() @app.get("/protected") async def protected_route( credentials: HTTPAuthorizationCredentials = Security(security) ): token = credentials.credentials # the Bearer token return {"token": token}
python — oauth2 password flow (full swagger login)
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi import Depends # Tells Swagger where to POST for tokens oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends()): # Validate credentials, return JWT return {"access_token": "jwt-token-here", "token_type": "bearer"} @app.get("/me") async def get_me(token: str = Depends(oauth2_scheme)): # Swagger UI shows a login form + auto-adds Bearer header return {"user": "decoded from token"}
python — api key in header (swagger authorize)
from fastapi.security import APIKeyHeader from fastapi import Security, HTTPException, status api_key_header = APIKeyHeader(name="X-API-Key") async def verify_api_key(api_key: str = Security(api_key_header)): if api_key != "secret-key-123": raise HTTPException(status_code=403, detail="Invalid API Key") return api_key @app.get("/secure-data") async def secure(key=Depends(verify_api_key)): # Swagger shows "X-API-Key" input in the Authorize dialog return {"data": "super secret"}
When you use
OAuth2PasswordBearer, Swagger UI automatically adds a username/password login form in the Authorize dialog — great for demoing and testing your auth flow.17.3
ReDoc
ReDoc is a beautiful read-only API documentation UI served at
/redoc. Unlike Swagger UI, it's not interactive — it's designed for readability and sharing. Three-panel layout: navigation, main content, and code samples.
Swagger UI — /docs
Interactive — try requests live in browser. Best for developers testing the API during development.
ReDoc — /redoc
Beautiful, readable, three-column layout. Best for sharing API docs with external teams or publishing publicly.
Configuration
▼
Just like Swagger UI, you can disable the default ReDoc route and mount your own with full control over CDN URLs and ReDoc config options.
python — custom redoc route
from fastapi import FastAPI from fastapi.openapi.docs import get_redoc_html from fastapi.responses import HTMLResponse app = FastAPI(redoc_url=None) # disable default @app.get("/redoc", include_in_schema=False) async def custom_redoc() -> HTMLResponse: return get_redoc_html( openapi_url="/openapi.json", title="My API — Reference Docs", redoc_js_url="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js", redoc_favicon_url="/static/favicon.png", with_google_fonts=True, )
For more control, you can write the ReDoc HTML entirely yourself and inject it as an
HTMLResponse. This is useful when you want to pass ReDoc config options (like hideDownloadButton, pathInMiddlePanel):
python — full custom redoc with config options
@app.get("/redoc", include_in_schema=False) async def custom_redoc(): html = """ <!DOCTYPE html> <html> <head> <title>My API Docs</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> </head> <body> <redoc spec-url='/openapi.json' hide-download-button path-in-middle-panel no-auto-auth native-scrollbars ></redoc> <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script> </body> </html> """ return HTMLResponse(html)
| ReDoc Option | Effect |
|---|---|
hide-download-button | Hides the "Download" spec button |
path-in-middle-panel | Shows paths in the center panel |
no-auto-auth | Disables automatic auth highlighting |
native-scrollbars | Uses browser scrollbars instead of custom |
expand-responses="200,201" | Pre-expands response codes |
required-props-first | Shows required fields at the top |
Branding
▼
ReDoc supports a theme object via JavaScript that lets you fully control colors, typography, spacing, and sidebar appearance — so docs can match your brand perfectly.
python — branded redoc with theme
@app.get("/redoc", include_in_schema=False) async def branded_redoc(): html = """ <!DOCTYPE html> <html> <head> <title>Acme API Docs</title> <meta charset="utf-8"/> <style> body { margin: 0; padding: 0; } </style> </head> <body> <div id="redoc-container"></div> <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script> <script> Redoc.init( '/openapi.json', { theme: { colors: { primary: { main: '#6c63ff' }, // brand purple success: { main: '#00d4aa' }, error: { main: '#ff6b6b' }, }, sidebar: { backgroundColor: '#0d0f14', textColor: '#e8eaf0', }, typography: { fontSize: '15px', fontFamily: 'Inter, sans-serif', headings: { fontFamily: 'Sora, sans-serif', fontWeight: '700', }, code: { fontFamily: 'JetBrains Mono, monospace', backgroundColor: '#0a0c12', } }, logo: { gutter: '20px', }, }, hideDownloadButton: true, pathInMiddlePanel: false, requiredPropsFirst: true, }, document.getElementById('redoc-container') ); </script> </body> </html> """ return HTMLResponse(html)
Add a logo to your API docs using the
x-logo OpenAPI extension in your schema:python — adding logo via openapi extension
from fastapi import FastAPI from fastapi.openapi.utils import get_openapi app = FastAPI() def custom_openapi(): if app.openapi_schema: return app.openapi_schema schema = get_openapi( title="Acme API", version="1.0.0", routes=app.routes, ) # Add logo for ReDoc (x-logo extension) schema["info"]["x-logo"] = { "url": "https://myapi.com/logo.png", "backgroundColor": "#0d0f14", "altText": "Acme API logo", } app.openapi_schema = schema return app.openapi_schema # Override the default schema generator app.openapi = custom_openapi
The
custom_openapi() pattern (overriding app.openapi) is also the right place to add global security schemes, custom extensions, or modify any part of the generated JSON schema.Complete real-world example — full custom docs setup:
python — production-grade docs config
from fastapi import FastAPI from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html from fastapi.openapi.utils import get_openapi from fastapi.staticfiles import StaticFiles import os IS_PROD = os.getenv("APP_ENV") == "production" app = FastAPI( docs_url=None, # disable defaults — we mount our own redoc_url=None, ) # Serve static files (logo, favicon) app.mount("/static", StaticFiles(directory="static"), name="static") if not IS_PROD: @app.get("/docs", include_in_schema=False) async def swagger_ui(): return get_swagger_ui_html( openapi_url="/openapi.json", title="Acme API — Swagger UI", swagger_ui_parameters={"filter": True, "tryItOutEnabled": True}, ) @app.get("/redoc", include_in_schema=False) async def redoc_ui(): return get_redoc_html( openapi_url="/openapi.json", title="Acme API — Reference", ) def custom_openapi(): if app.openapi_schema: return app.openapi_schema schema = get_openapi(title="Acme API", version="2.0.0", routes=app.routes) schema["info"]["x-logo"] = {"url": "/static/logo.png"} app.openapi_schema = schema return app.openapi_schema app.openapi = custom_openapi
Topic 17 Complete! You now know how FastAPI auto-generates OpenAPI schemas, how to enrich them with Field metadata, tags and descriptions, how to customise Swagger UI (CDN, parameters, security schemes), and how to brand ReDoc with custom themes and logos. Reply "next" to continue to Topic 18: Testing.