Skip to content

OpenAPI

Leverage FastAPI's automatic OpenAPI documentation.

Automatic Generation

FastAPI automatically generates OpenAPI schemas from your code:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(
    title="My API",
    description="A sample API",
    version="1.0.0",
)

class Item(BaseModel):
    name: str
    price: float

@app.get("/items/{item_id}")
async def get_item(item_id: int) -> Item:
    """Get an item by ID."""
    pass

Access documentation at: - Swagger UI: http://localhost:8000/docs - ReDoc: http://localhost:8000/redoc - OpenAPI JSON: http://localhost:8000/openapi.json

Schema Customization

App-Level Configuration

from fastapi import FastAPI

app = FastAPI(
    title="Sartiq API",
    description="""
## Overview

The Sartiq API provides programmatic access to photography services.

## Authentication

All endpoints require a Bearer token in the Authorization header.

## Rate Limits

- 100 requests/minute for standard users
- 1000 requests/minute for premium users
    """,
    version="2.0.0",
    terms_of_service="https://example.com/terms",
    contact={
        "name": "API Support",
        "url": "https://example.com/support",
        "email": "api@example.com",
    },
    license_info={
        "name": "MIT",
        "url": "https://opensource.org/licenses/MIT",
    },
    openapi_tags=[
        {
            "name": "users",
            "description": "User management operations",
        },
        {
            "name": "photos",
            "description": "Photo upload and management",
        },
    ],
)

Endpoint Documentation

from fastapi import FastAPI, Path, Query, Body, HTTPException

@app.post(
    "/photos",
    response_model=Photo,
    status_code=201,
    summary="Upload a photo",
    description="""
Upload a new photo to the gallery.

The photo will be processed asynchronously and available within 30 seconds.
    """,
    response_description="The created photo object",
    tags=["photos"],
    responses={
        201: {
            "description": "Photo uploaded successfully",
            "content": {
                "application/json": {
                    "example": {
                        "id": 123,
                        "url": "https://cdn.example.com/photos/123.jpg",
                        "status": "processing",
                    }
                }
            },
        },
        413: {"description": "Photo too large (max 10MB)"},
        415: {"description": "Unsupported media type"},
    },
)
async def upload_photo(
    file: UploadFile,
    title: str = Query(..., description="Photo title", example="Sunset at Beach"),
    album_id: int = Query(None, description="Album to add photo to"),
):
    """
    Upload a photo with the following constraints:

    - **file**: JPEG, PNG, or WebP format
    - **max size**: 10MB
    - **title**: Required, 1-200 characters
    """
    pass

Model Documentation

from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional, List

class PhotoBase(BaseModel):
    """Base photo schema with common fields."""

    title: str = Field(
        ...,
        min_length=1,
        max_length=200,
        description="Photo title",
        example="Sunset at Beach",
    )
    description: Optional[str] = Field(
        None,
        max_length=2000,
        description="Detailed photo description",
        example="A beautiful sunset captured at Malibu Beach",
    )
    tags: List[str] = Field(
        default_factory=list,
        max_items=20,
        description="Tags for categorization",
        example=["sunset", "beach", "nature"],
    )

class PhotoCreate(PhotoBase):
    """Schema for creating a new photo."""
    pass

class Photo(PhotoBase):
    """Complete photo schema with all fields."""

    id: int = Field(..., description="Unique photo identifier")
    url: str = Field(..., description="CDN URL for the photo")
    thumbnail_url: str = Field(..., description="Thumbnail URL")
    width: int = Field(..., description="Width in pixels")
    height: int = Field(..., description="Height in pixels")
    created_at: datetime = Field(..., description="Upload timestamp")
    author_id: int = Field(..., description="ID of the uploader")

    model_config = {
        "json_schema_extra": {
            "example": {
                "id": 123,
                "title": "Sunset at Beach",
                "description": "A beautiful sunset",
                "tags": ["sunset", "beach"],
                "url": "https://cdn.example.com/photos/123.jpg",
                "thumbnail_url": "https://cdn.example.com/photos/123_thumb.jpg",
                "width": 1920,
                "height": 1080,
                "created_at": "2024-01-15T10:30:00Z",
                "author_id": 456,
            }
        }
    }

Parameter Documentation

from fastapi import Path, Query, Header, Cookie

@app.get("/users/{user_id}/photos")
async def get_user_photos(
    user_id: int = Path(
        ...,
        title="User ID",
        description="The ID of the user whose photos to retrieve",
        example=123,
        ge=1,
    ),
    page: int = Query(
        1,
        title="Page number",
        description="Page number for pagination",
        ge=1,
        example=1,
    ),
    per_page: int = Query(
        20,
        title="Items per page",
        description="Number of items per page",
        ge=1,
        le=100,
        example=20,
    ),
    sort: str = Query(
        "-created_at",
        title="Sort field",
        description="Field to sort by. Prefix with '-' for descending.",
        regex="^-?(created_at|title|views)$",
        example="-created_at",
    ),
    x_request_id: str = Header(
        None,
        alias="X-Request-ID",
        description="Request tracking ID",
    ),
):
    pass

Response Models

Multiple Response Types

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel

class ErrorResponse(BaseModel):
    error: str
    message: str
    details: Optional[dict] = None

@app.get(
    "/photos/{photo_id}",
    response_model=Photo,
    responses={
        404: {
            "model": ErrorResponse,
            "description": "Photo not found",
            "content": {
                "application/json": {
                    "example": {
                        "error": "not_found",
                        "message": "Photo with ID 123 not found",
                    }
                }
            },
        },
        403: {
            "model": ErrorResponse,
            "description": "Access denied",
        },
    },
)
async def get_photo(photo_id: int):
    photo = await db.get(Photo, photo_id)
    if not photo:
        raise HTTPException(404, detail="Photo not found")
    return photo

Union Response Types

from typing import Union

class SuccessResponse(BaseModel):
    success: bool = True
    data: Photo

class ErrorResponse(BaseModel):
    success: bool = False
    error: str

@app.post("/photos", response_model=Union[SuccessResponse, ErrorResponse])
async def create_photo(photo: PhotoCreate):
    try:
        result = await save_photo(photo)
        return SuccessResponse(data=result)
    except ValidationError as e:
        return ErrorResponse(error=str(e))

Security Schemes

Bearer Token

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

app = FastAPI()

# This adds the security scheme to OpenAPI
@app.get("/protected", dependencies=[Depends(security)])
async def protected_route(
    credentials: HTTPAuthorizationCredentials = Depends(security),
):
    """
    This endpoint requires a Bearer token.

    Obtain a token from `/auth/login`.
    """
    pass

OAuth2

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="auth/token",
    scopes={
        "read:photos": "Read photos",
        "write:photos": "Create and edit photos",
        "delete:photos": "Delete photos",
    },
)

@app.post("/auth/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    OAuth2 token endpoint.

    Use username and password to obtain an access token.
    """
    pass

@app.get("/photos", dependencies=[Depends(oauth2_scheme)])
async def list_photos():
    """Requires `read:photos` scope."""
    pass

API Key

from fastapi.security import APIKeyHeader, APIKeyQuery

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
api_key_query = APIKeyQuery(name="api_key", auto_error=False)

async def get_api_key(
    header_key: str = Depends(api_key_header),
    query_key: str = Depends(api_key_query),
):
    api_key = header_key or query_key
    if not api_key or not await verify_api_key(api_key):
        raise HTTPException(401, "Invalid API key")
    return api_key

Grouping with Tags

from fastapi import APIRouter

# Create routers with tags
users_router = APIRouter(prefix="/users", tags=["users"])
photos_router = APIRouter(prefix="/photos", tags=["photos"])
admin_router = APIRouter(prefix="/admin", tags=["admin"])

@users_router.get("/")
async def list_users():
    """List all users."""
    pass

@photos_router.get("/")
async def list_photos():
    """List all photos."""
    pass

@admin_router.get("/stats")
async def get_stats():
    """Get system statistics (admin only)."""
    pass

# Include routers
app.include_router(users_router)
app.include_router(photos_router)
app.include_router(admin_router)

Customizing OpenAPI Output

Hide Endpoints

@app.get("/internal/health", include_in_schema=False)
async def health_check():
    """Not shown in docs."""
    return {"status": "ok"}

Custom OpenAPI Schema

from fastapi.openapi.utils import get_openapi

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title="My API",
        version="1.0.0",
        description="Custom description",
        routes=app.routes,
    )

    # Add custom server
    openapi_schema["servers"] = [
        {"url": "https://api.example.com", "description": "Production"},
        {"url": "https://staging-api.example.com", "description": "Staging"},
    ]

    # Add custom info
    openapi_schema["info"]["x-logo"] = {
        "url": "https://example.com/logo.png"
    }

    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi

Exporting OpenAPI Schema

# Export to JSON
python -c "from app.main import app; import json; print(json.dumps(app.openapi()))" > openapi.json

# Export to YAML
pip install pyyaml
python -c "
from app.main import app
import yaml
print(yaml.dump(app.openapi(), sort_keys=False))
" > openapi.yaml

Best Practices

Practice Implementation
Use descriptive summaries Short, action-oriented titles
Document all parameters Description, example, constraints
Show example responses Realistic examples in schema
Group with tags Logical grouping of endpoints
Include error responses Document all possible errors
Use consistent models Reuse schemas across endpoints

See Also