Skip to content

Instantly share code, notes, and snippets.

@Kludex
Last active April 17, 2024 01:55
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Kludex/1c515aa38d22ec28d514da6b6f36da9f to your computer and use it in GitHub Desktop.
Save Kludex/1c515aa38d22ec28d514da6b6f36da9f to your computer and use it in GitHub Desktop.
Document each version on FastAPI
from fastapi import APIRouter, FastAPI
from utils import create_versioning_docs
app = FastAPI(docs_url=None, redoc_url=None)
v1_router = APIRouter(prefix="/v1")
v2_router = APIRouter(prefix="/v2")
create_versioning_docs(v1_router)
create_versioning_docs(v2_router)
@v1_router.get("/")
def get_hello_world():
return {"message": "Hello World"}
@v2_router.get("/")
def get_another_world():
return {"message": "Another World"}
app.include_router(v1_router)
app.include_router(v2_router)
fastapi==0.68.0
uvicorn==0.14.0
import copy
from collections import defaultdict
from typing import Any, DefaultDict, Dict, Optional
from fastapi import APIRouter, Request
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
custom_openapi: DefaultDict[str, Optional[Dict[str, Any]]] = defaultdict(lambda: None)
def create_versioning_docs(router: APIRouter) -> None:
prefix = router.prefix
@router.get("/openapi.json", include_in_schema=False, name=f"{prefix}_openapi")
async def get_openapi_json(request: Request):
global custom_openapi
version = request.url.path.strip("/").split("/")[0]
if custom_openapi[version] is None:
custom_openapi[version] = copy.deepcopy(request.app.openapi())
# Remove other version tags on openapi schema.
for path in custom_openapi[version]["paths"].copy():
if not path.startswith(f"/{version}"):
del custom_openapi[version]["paths"][path]
return custom_openapi[version]
@router.get("/docs", include_in_schema=False, name=f"{prefix}_swagger")
async def get_swagger(request: Request):
return get_swagger_ui_html(
openapi_url=f"{prefix}/openapi.json",
title=request.app.title + " - Swagger UI",
)
@router.get("/redoc", include_in_schema=False, name=f"{prefix}_redoc")
async def redoc_html(request: Request):
return get_redoc_html(
openapi_url=f"{prefix}/openapi.json", title=request.app.title + " - ReDoc"
)
@vicchi
Copy link

vicchi commented Sep 8, 2021

@Kludex thanks so much for this; it's been awesomely helpful. I did discover one bug-lette in the get_openapi_json function where if you have two versions running concurrently, hitting the first versioned docs endpoint in your browser effectively nukes the docs for the second versioned docs endpoint.

Changing utils.py:19 to custom_openapi[version] = copy.deepcopy(request.app.openapi()) resolves this.

See https://gist.github.com/vicchi/81213006e2fd7049da9e3aa10728af06 for a working version, including a (slightly hacky) way of overriding the OpenAPI info to have version specific overrides in the generated docs.

@Kludex
Copy link
Author

Kludex commented Sep 8, 2021

Hey @vicchi! Thanks for pointing out the issue! :)

Should be fine by now, can you confirm?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment