Examples¶
Every example is a complete, runnable app, and each one is imported and checked by the test suite. Run one with uvicorn examples.<name>:app --reload from a checkout of the repository.
semver_app.py¶
Full SemVer lifecycle: introduce, deprecate, remove, permanent deprecation, multi-method route with partial takeover
Show the code
"""
SemVer lifecycle example.
Full endpoint lifecycle (introduce, deprecate, remove, permanent deprecation) plus a
multi-method route with a partial takeover, using Semantic Versioning tuples.
Run:
uvicorn examples.semver_app:app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
app = FastAPI(
title="SemVer Lifecycle API",
description="Demonstration of endpoint lifecycles using Semantic Versioning tuples.",
)
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
# 1. Introduced in v1.0, persists across all versions
@router.get("/persistent")
@api_version((1, 0))
def persistent_route() -> dict[str, str]:
return {"status": "active", "message": "I persist across all versions."}
# 2. Added in v2.0
@router.get("/newcomer")
@api_version((2, 0))
def added_in_next_version() -> dict[str, str]:
return {"status": "active", "message": "I was added in v2.0."}
# 3. Introduced in v1.0, deprecated in v2.0, removed in v3.0
@router.get("/lifecycle")
@api_version((1, 0), deprecate_in=(2, 0), remove_in=(3, 0))
def lifecycle_route() -> dict[str, str]:
return {
"status": "active",
"message": "I am stable in v1.0, deprecated in v2.0, and removed in v3.0.",
}
# 4. Added in v3.0
@router.get("/future")
@api_version((3, 0))
def future_route() -> dict[str, str]:
return {"status": "active", "message": "Welcome to v3.0!"}
# 5. Introduced in v1.0, deprecated in v2.0, with no remove_in: it stays deprecated forever,
# there's no removal planned.
@router.get("/legacy-notice")
@api_version((1, 0), deprecate_in=(2, 0))
def legacy_notice_route() -> dict[str, str]:
return {"status": "deprecated", "message": "I was deprecated in v2.0, but I'm never removed."}
# 6. A single route handling both GET and POST from v1.0. In v2.0, a dedicated route takes
# over POST alone; GET keeps being served by this same handler in both versions.
# FastAPI warns about a duplicate operation ID for this route regardless of versioning
# (reproducible with plain FastAPI too); it's a cosmetic quirk of multi-method routes, safe
# to ignore here.
@router.api_route("/settings", methods=["GET", "POST"])
@api_version((1, 0))
def settings_route() -> dict[str, str]:
return {"handler": "settings_v1", "message": "I handle both GET and POST."}
@router.post("/settings")
@api_version((2, 0))
def settings_post_v2() -> dict[str, str]:
return {"handler": "settings_post_v2", "message": "I took over POST /settings from v2.0."}
@router.post("/items")
@api_version((1, 0))
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
calver_app.py¶
Introduce, deprecate and remove, with CalVer date strings instead of SemVer tuples
Show the code
"""
CalVer lifecycle example.
Full endpoint lifecycle (introduce, deprecate, remove) using Calendar Versioning strings
instead of SemVer tuples.
Run:
uvicorn examples.calver_app:app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
app = FastAPI(
title="CalVer Lifecycle API",
description="Demonstration of endpoint lifecycles using Calendar Versioning strings.",
)
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
# 1. Introduced in the January release, persists across all versions
@router.get("/persistent")
@api_version("2025-01-01")
def persistent_route() -> dict[str, str]:
return {"status": "active", "message": "I persist across all versions."}
# 2. Added in the June release
@router.get("/newcomer")
@api_version("2025-06-01")
def added_in_next_version() -> dict[str, str]:
return {"status": "active", "message": "I was added in 2025-06-01."}
# 3. Introduced in January, deprecated in June, removed in December
@router.get("/lifecycle")
@api_version("2025-01-01", deprecate_in="2025-06-01", remove_in="2025-12-01")
def lifecycle_route() -> dict[str, str]:
return {
"status": "active",
"message": "I am stable in Jan, deprecated in Jun, and removed in Dec.",
}
# 4. Added in the December release
@router.get("/future")
@api_version("2025-12-01")
def future_route() -> dict[str, str]:
return {"status": "active", "message": "Welcome to the December release!"}
@router.post("/items")
@api_version("2025-01-01")
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.CALVER,
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
semver_major_only_app.py¶
Major-only URLs (/v1, /v2) via prefix_format
Show the code
"""
SemVer major-only example.
Demonstrates major-only versioning via prefix_format: /v1, /v2, /v3 instead of the
default /v1_0, /v2_0.
Run:
uvicorn examples.semver_major_only_app:app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
app = FastAPI(
title="SemVer Major-Only API",
description="Demonstrates major-only versioning: /v1, /v2, /v3 instead of /v1_0, /v2_0.",
)
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
# 1. Introduced in v1, persists across all versions
@router.get("/persistent")
@api_version((1, 0))
def persistent_route() -> dict[str, str]:
return {"status": "active", "message": "I persist across all versions."}
# 2. Added in v2
@router.get("/newcomer")
@api_version((2, 0))
def added_in_v2() -> dict[str, str]:
return {"status": "active", "message": "I was added in v2."}
# 3. Introduced in v1, deprecated in v2, removed in v3
@router.get("/lifecycle")
@api_version((1, 0), deprecate_in=(2, 0), remove_in=(3, 0))
def lifecycle_route() -> dict[str, str]:
return {
"status": "active",
"message": "I am stable in v1, deprecated in v2, and removed in v3.",
}
# 4. Added in v3
@router.get("/future")
@api_version((3, 0))
def future_route() -> dict[str, str]:
return {"status": "active", "message": "Welcome to v3!"}
@router.post("/items")
@api_version((1, 0))
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
prefix_format="/v{major}",
semantic_version_format="{major}",
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
deprecation_headers_app.py¶
Deprecation, Sunset and Link response headers via deprecation_headers and version_info
Show the code
"""
Runtime deprecation signalling example.
The declarative lifecycle (`deprecate_in` / `remove_in`) already flags routes as deprecated
in the per-version OpenAPI. Turn on `deprecation_headers=True` and `RouterVersioner` adds
RFC-conformant headers to every response of a route in its deprecation window. `version_info`,
one `VersionInfo` per version, supplies the dates and guide URLs:
VersionInfo.release_date -> Deprecation (RFC 9745) at deprecate_in, Sunset (RFC 8594) at remove_in
VersionInfo.guide -> Link: <url>; rel="deprecation" (RFC 9745) at deprecate_in
Plus an automatic `Link: ...; rel="successor-version"` whenever a later version still serves
the same path. The header values are fixed at versionize() time (only the successor link's
root_path prefix is filled per request): no second decorator, no middleware, no `Depends()`.
Try:
curl -i http://127.0.0.1:8000/v1_0/report # not yet deprecated -> no headers
curl -i http://127.0.0.1:8000/v2_0/report # deprecated, removal dated -> Deprecation + Sunset
curl -i http://127.0.0.1:8000/v3_0/report # removed -> 404
curl -i http://127.0.0.1:8000/v2_0/ping # deprecated, no removal -> Deprecation + successor Link
Run:
uvicorn examples.deprecation_headers_app:app --reload
"""
from datetime import date
from fastapi import APIRouter, FastAPI
from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionInfo, api_version
app = FastAPI(
title="Deprecation Signalling API",
description="Declarative lifecycle turned into RFC 9745 / RFC 8594 response headers.",
)
router = APIRouter()
# Stable across every version: never carries deprecation headers.
@router.get("/health")
@api_version((1, 0))
def health() -> dict[str, str]:
return {"status": "ok"}
# Introduced in v1, deprecated in v2, removed in v3. The same handler serves it in v1 and v2
# (nothing replaces it), so v2 is a genuine deprecation window: responses there carry
# Deprecation (from version_info[(2, 0)].release_date), Sunset (from version_info[(3, 0)].
# release_date), and a Link to the v2 upgrade guide. No successor Link - the endpoint is
# being retired.
@router.get("/report")
@api_version((1, 0), deprecate_in=(2, 0), remove_in=(3, 0))
def report() -> dict[str, str]:
return {"report": "flat, unpaginated"}
# Introduced in v1, deprecated in v2, never removed: it lives on in v3. From v2 its responses
# carry Deprecation, the v2 upgrade-guide Link, and a Link: rel="successor-version" pointing
# at /v3_0/ping. No Sunset - no removal is planned.
@router.get("/ping")
@api_version((1, 0), deprecate_in=(2, 0))
def ping() -> dict[str, str]:
return {"pong": "still here"}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
deprecation_headers=True,
version_info={
(1, 0): VersionInfo(release_date=date(2024, 1, 15)),
(2, 0): VersionInfo(
release_date=date(2025, 3, 1),
guide="https://api.example.com/docs/upgrade/v2",
),
(3, 0): VersionInfo(release_date=date(2026, 1, 1)),
},
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
webhook_versioning_app.py¶
Per-version webhook definitions via webhook_routers
Show the code
"""
Versioned webhooks example.
Demonstrates per-version webhook definitions via webhook_routers — webhook payloads evolve
and get removed across versions using the same @api_version lifecycle as regular routes.
Run:
uvicorn examples.webhook_versioning_app:app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
app = FastAPI(
title="Versioned Webhooks Demo",
description="Demonstrates per-version webhook definitions using webhook_routers.",
)
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
@router.post("/orders")
@api_version((1, 0))
def create_order_v1() -> dict[str, str]:
return {"version": "1.0", "status": "created"}
@router.post("/orders")
@api_version((2, 0))
def create_order_v2() -> dict[str, str]:
return {"version": "2.0", "status": "accepted"}
@router.get("/items")
@api_version((1, 0))
def get_items() -> dict[str, object]:
return {"items": ["a", "b"]}
@router.post("/items")
@api_version((1, 0))
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
# Webhooks document outbound calls your API makes to subscriber URLs.
# Annotate them with @api_version just like regular routes.
webhook_router = APIRouter()
# Introduced in v1.0: basic order-created notification.
@webhook_router.post("/order-created")
@api_version((1, 0))
def on_order_created_v1(body: dict[str, str]) -> None:
"""
Payload sent to subscribers when an order is created.
v1 body: {"order_id": "...", "status": "created"}
"""
# v2.0 replaces the v1 definition (same path + method → only one entry per version).
# Subscribers on v2 receive an extended payload with the customer field.
@webhook_router.post("/order-created")
@api_version((2, 0))
def on_order_created_v2(body: dict[str, str]) -> None:
"""
Payload sent to subscribers when an order is created.
v2 body: {"order_id": "...", "status": "accepted", "customer_id": "..."}
"""
# Introduced in v1.0, removed in v2.0 (replaced by order-created).
@webhook_router.post("/payment-received")
@api_version((1, 0), remove_in=(2, 0))
def on_payment_received(body: dict[str, str]) -> None:
"""
Fired when a payment is confirmed. Removed in v2 — subscribe to order-created instead.
"""
versioner = RouterVersioner(
app=app,
routers=router,
webhook_routers=webhook_router,
version_format=VersionFormat.SEMVER,
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
# Result:
# /v1_0/openapi.json → webhooks: /order-created (v1 payload), /payment-received
# /v2_0/openapi.json → webhooks: /order-created (v2 payload) ← /payment-received removed
multi_router_app.py¶
Several routers versioned together under one instance
Show the code
"""
Multi-router example.
Shows how to version routes split across multiple APIRouters — the typical
real-world pattern where each domain (users, products, …) lives in its own
router/module. Pass them as a list to RouterVersioner and they are all
versioned together under a single prefix tree.
Run:
uvicorn examples.multi_router_app:app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
app = FastAPI(
title="Multi-Router API",
description="Routes split across multiple routers and versioned together.",
)
users_router = APIRouter(prefix="/users", tags=["Users"])
products_router = APIRouter(prefix="/products", tags=["Products"])
class CreateProductRequest(BaseModel):
name: str
quantity: int
@users_router.get("/")
@api_version((1, 0))
def list_users() -> dict[str, list[str]]:
return {"users": []}
@users_router.get("/profile")
@api_version((2, 0))
def user_profile() -> dict[str, str]:
return {"profile": "extended profile added in v2.0"}
@products_router.get("/")
@api_version((1, 0))
def list_products() -> dict[str, list[str]]:
return {"products": []}
@products_router.get("/search")
@api_version((2, 0))
def search_products() -> dict[str, list[str]]:
return {"results": []}
@products_router.post("/")
@api_version((1, 0))
def create_product(body: CreateProductRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
versioner = RouterVersioner(
app=app,
routers=[users_router, products_router], # list of routers versioned together
version_format=VersionFormat.SEMVER,
latest_prefix="/latest",
include_versions_route=True,
)
versioner.versionize()
self_hosted_docs_app.py¶
Swagger UI and ReDoc served from local static assets
Show the code
"""
Self-hosted (air-gapped) docs example.
In air-gapped environments or corporate networks where CDN access is blocked,
serve Swagger UI and ReDoc assets from your own server instead of the default CDN.
Setup (one time):
python examples/download_static_assets.py # downloads assets to examples/static/
pip install aiofiles # required by FastAPI StaticFiles
Then run:
uvicorn examples.self_hosted_docs_app:app --reload
"""
from pathlib import Path
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
STATIC_DIR = Path(__file__).parent / "static"
app = FastAPI(
title="Self-Hosted Docs API",
description="Swagger UI and ReDoc served from local static assets — no CDN required.",
)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
@router.get("/items")
@api_version((1, 0))
def list_items() -> dict[str, list[str]]:
return {"items": []}
@router.post("/items")
@api_version((1, 0))
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
swagger_favicon_url="/static/favicon.png",
redoc_js_url="/static/redoc.standalone.js",
redoc_favicon_url="/static/favicon.png",
redoc_with_google_fonts=False, # avoids loading fonts.googleapis.com
)
versioner.versionize()
openapi_hook_app.py¶
Per-version OpenAPI schema edits via openapi_hook
Show the code
"""
OpenAPI hook example.
Demonstrates per-version OpenAPI schema customization via openapi_hook — adding a logo to
every version's schema, plus version-specific description text.
Run:
uvicorn examples.openapi_hook_app:app --reload
"""
from typing import Any
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionT, api_version
app = FastAPI(
title="OpenAPI Hook Demo",
description="Demonstrates per-version OpenAPI schema customization via openapi_hook.",
)
router = APIRouter()
class CreateItemRequest(BaseModel):
name: str
quantity: int
@router.get("/items")
@api_version((1, 0))
def get_items_v1() -> dict[str, object]:
return {"version": "1.0", "items": ["a", "b"]}
@router.get("/items")
@api_version((2, 0))
def get_items_v2() -> dict[str, object]:
return {"version": "2.0", "items": ["a", "b", "c"]}
@router.get("/users")
@api_version((1, 0), deprecate_in=(2, 0))
def get_users() -> dict[str, object]:
return {"users": ["alice", "bob"]}
@router.post("/items")
@api_version((1, 0))
def create_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
def my_openapi_hook(schema: dict[str, Any], version: VersionT) -> dict[str, Any]:
# Applied to every version: add a company logo to the docs
schema["info"]["x-logo"] = {"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"}
# Version-specific customization
if version == (1, 0):
schema["info"]["description"] = (
schema["info"].get("description") or ""
) + "\n\n> **Note:** v1 is in maintenance mode. Migrate to v2 when ready."
elif version == (2, 0):
schema["info"]["description"] = (schema["info"].get("description") or "") + "\n\n> **Current stable release.**"
return schema
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
latest_prefix="/latest",
include_versions_route=True,
openapi_hook=my_openapi_hook,
)
versioner.versionize()
versions_dashboard_app.py¶
GET /versions JSON and the GET /dashboard HTML page side by side
Show the code
"""
Versions dashboard example.
Enables both discovery surfaces side by side:
- GET /versions JSON list of every active version (include_versions_route)
- GET /dashboard HTML page with the same versions and links to their docs
(include_versions_dashboard)
Both also list a version's migration guide when version_info gives one for it (this needs
no deprecation_headers). The dashboard works with or without the JSON route. Its path moves
with versions_dashboard_path, and versions_dashboard_hook(version_models, root_path) -> str
replaces the built-in page entirely if you want your own markup.
Run:
uvicorn examples.versions_dashboard_app:app --reload
then open http://127.0.0.1:8000/dashboard
"""
from fastapi import APIRouter, FastAPI
from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionInfo, api_version
app = FastAPI(title="Versions Dashboard Demo")
router = APIRouter()
@router.get("/items")
@api_version((1, 0))
def get_items_v1() -> dict[str, object]:
return {"version": "1.0", "items": ["a", "b"]}
@router.get("/items")
@api_version((2, 0))
def get_items_v2() -> dict[str, object]:
return {"version": "2.0", "items": ["a", "b", "c"]}
@router.get("/reports")
@api_version((2, 0))
def get_reports() -> dict[str, object]:
return {"reports": []}
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
latest_prefix="/latest",
include_versions_route=True,
include_versions_dashboard=True,
version_info={(2, 0): VersionInfo(guide="https://api.example.com/docs/upgrade/v2")},
)
versioner.versionize()
versions_dashboard_hook_app.py¶
Custom dashboard via versions_dashboard_hook: Jinja template with a separate stylesheet file
Show the code
"""
Custom versions dashboard via versions_dashboard_hook.
versions_dashboard_hook(version_models, root_path) -> str replaces the built-in dashboard
page entirely. Here it renders a Jinja template from examples/dashboard_assets/, with the
stylesheet kept as a separate file served from the same directory rather than inlined in
the page.
- version_models: the same list GET /versions returns under "versions". Each entry has
"version" and, when the per-version docs exist, "swagger_url" / "redoc_url" /
"openapi_url" (already root_path-prefixed), plus "guide_url" for the versions
version_info gives a migration guide for.
- root_path: the request root_path ("" when there is none); used here only to build the
stylesheet URL.
The hook is not handed the app title/version, so this one reads app.title and app.version
off the module-level app and passes them to the template, the way the built-in page does.
Setup:
pip install jinja2
Run:
uvicorn examples.versions_dashboard_hook_app:app --reload
then open http://127.0.0.1:8000/dashboard
"""
from pathlib import Path
from typing import Any
import jinja2
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
ASSETS_DIR = Path(__file__).parent / "dashboard_assets"
_templates = jinja2.Environment(
loader=jinja2.FileSystemLoader(ASSETS_DIR),
autoescape=jinja2.select_autoescape(["html"]),
)
app = FastAPI(title="Dashboard Hook Demo")
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
router = APIRouter()
@router.get("/items")
@api_version((1, 0))
def get_items_v1() -> dict[str, object]:
return {"version": "1.0", "items": ["a", "b"]}
@router.get("/items")
@api_version((2, 0))
def get_items_v2() -> dict[str, object]:
return {"version": "2.0", "items": ["a", "b", "c"]}
@router.get("/reports")
@api_version((2, 0))
def get_reports() -> dict[str, object]:
return {"reports": []}
def render_dashboard(version_models: list[dict[str, Any]], root_path: str) -> str:
return _templates.get_template("versions_dashboard.html").render(
versions=version_models,
root_path=root_path,
title=app.title,
version=app.version,
)
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
include_versions_dashboard=True,
versions_dashboard_hook=render_dashboard,
)
versioner.versionize()
mounted_subapps_app.py¶
Independently versioned modules as separate app.mount() sub-applications
Show the code
"""
Sub-applications example.
Shows how to mount independently versioned modules as separate FastAPI
sub-applications via app.mount() — each with its own RouterVersioner, docs,
and OpenAPI schema. RouterVersioner already threads the ASGI root_path through
docs/openapi URLs automatically, so no extra configuration is needed.
Run:
uvicorn examples.mounted_subapps_app:main_app --reload
"""
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
class CreateItemRequest(BaseModel):
name: str
quantity: int
main_app = FastAPI(title="Main App")
# Admin sub-app: SemVer
admin_app = FastAPI(title="Admin API")
admin_router = APIRouter()
@admin_router.get("/users")
@api_version((1, 0))
def list_admin_users() -> dict[str, list[str]]:
return {"users": []}
@admin_router.post("/items")
@api_version((1, 0))
def create_admin_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
RouterVersioner(app=admin_app, routers=admin_router, version_format=VersionFormat.SEMVER).versionize()
# Orders sub-app: CalVer
orders_app = FastAPI(title="Orders API")
orders_router = APIRouter()
@orders_router.get("/")
@api_version("2025-01-01")
def list_orders() -> dict[str, list[str]]:
return {"orders": []}
@orders_router.post("/items")
@api_version("2025-01-01")
def create_order_item(body: CreateItemRequest) -> dict[str, str]:
return {"name": body.name, "quantity": str(body.quantity)}
RouterVersioner(app=orders_app, routers=orders_router, version_format=VersionFormat.CALVER).versionize()
main_app.mount("/admin", admin_app)
main_app.mount("/orders", orders_app)
validation_override_integration_app.py¶
Custom validation error status code via fastapi-validation-override and openapi_hook
Show the code
"""
Custom validation error status via fastapi-validation-override.
RouterVersioner does not have a built-in way to change FastAPI's default 422
status code: that is a separate, general-purpose concern handled by the
fastapi-validation-override package. This example wires the two together:
1. override_validation_error() patches the app once, at runtime and on its own
root /openapi.json.
2. openapi_hook re-applies patch_422_responses() to the schema RouterVersioner
generates for each version, so every versioned /vX_Y/openapi.json reflects
the same status code too.
Run:
uvicorn examples.validation_override_integration_app:app --reload
"""
from typing import Any
from fastapi import APIRouter, FastAPI
from fastapi_validation_override import override_validation_error, patch_422_responses
from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionT, api_version
app = FastAPI(title="Validation Override Integration Demo")
router = APIRouter()
@router.get("/items")
@api_version((1, 0))
def get_items_v1(count: int) -> dict[str, str]:
return {"count": str(count)}
@router.get("/items")
@api_version((2, 0))
def get_items_v2(count: int) -> dict[str, str]:
return {"count": str(count)}
override_validation_error(app, status_code=400)
def versioning_openapi_hook(schema: dict[str, Any], version: VersionT) -> dict[str, Any]:
return patch_422_responses(schema, "400")
versioner = RouterVersioner(
app=app,
routers=router,
version_format=VersionFormat.SEMVER,
openapi_hook=versioning_openapi_hook,
)
versioner.versionize()
Support files¶
download_static_assets.py downloads the Swagger UI and ReDoc files used by self_hosted_docs_app.py, and the dashboard_assets/ folder holds the template and stylesheet of versions_dashboard_hook_app.py.