FastAPI Mastery
Topic 17 of 22 — OpenAPI & Documentation
77.3% complete
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.
https://myapi.com/openapi.json
My Awesome API — v2.1.0
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 OptionEffect
hide-download-buttonHides the "Download" spec button
path-in-middle-panelShows paths in the center panel
no-auto-authDisables automatic auth highlighting
native-scrollbarsUses browser scrollbars instead of custom
expand-responses="200,201"Pre-expands response codes
required-props-firstShows 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.