OpenAPI Customizations & Tags
Learn to customize the OpenAPI schema in FastAPI: group endpoints with tags, add metadata, and improve API documentation.
Focus: building openapi customizations and tags
You've built a working FastAPI app, but as your routes multiply, the automatically generated docs start to look like a tangled web — every endpoint listed flat, no grouping, no context. Worse, the OpenAPI schema behind that docs page is almost bare, missing the metadata that makes your API consumable by other developers and tools. This lesson fixes that pain by teaching you how to customize the OpenAPI schema and group endpoints with tags, turning chaotic documentation into a clean, professional API surface that's ready for the world.
The problem this lesson solves
When your API grows beyond a handful of endpoints, the default OpenAPI documentation becomes a wall of endpoints. Clients, colleagues, and even you struggle to find the right route. The official FastAPI docs will list everything in a single flat list, which is fine for a demo but poor for a real project.
The deeper problem: the OpenAPI schema itself is your API's contract. A contract that's missing descriptions, version info, and grouping is like a specifications doc with no table of contents. It's not just a documentation issue — tools that generate client SDKs, validate requests, or run integration tests read that schema. If it's unclear, those tools produce unclear results.
Here’s what we're going to fix:
- Endpoints scattered with no logical grouping — tags solve this.
- No version or description on the API itself — OpenAPI metadata solves this.
- Each operation (endpoint) lacks explanation — custom descriptions and summaries solve this.
- You want finer control over the schema that's generated — custom OpenAPI patching solves this.
By the end of this lesson, your /docs page will look like a professional product, not a prototype.
Core concept / mental model
Think of OpenAPI as a specification document — a living, machine-readable description of your API. FastAPI generates this automatically from your code. Tags and customizations are like adding a table of contents, chapter titles, and a cover page to that document.
Tags are labels you attach to operations (endpoints). When FastAPI builds the OpenAPI schema, it groups operations by tag. This is what makes the docs page show collapsible sections like Users, Items, or Admin.
OpenAPI customizations go beyond grouping. They include:
- API-level metadata: title, version, description, contact info, license.
- Operation-level details: summaries, descriptions, response descriptions.
- Schema-level tweaks: adding custom fields or overriding parts of the generated schema.
A mental model: your FastAPI app is the source of truth. FastAPI introspects your routes, Pydantic models, and docstrings to build an OpenAPI dictionary (a Python dict). That dict then gets served at /openapi.json. Tags and customizations are ways to shape that dict.
Pro tip: Always remember — the OpenAPI schema is generated, but it's just a Python dict. That means you can modify it programmatically before it's served. That's the ultimate customization escape hatch.
How it works step by step
Let's walk through the mechanics, from code to documentation.
Step 1: Defining the FastAPI app with metadata
When you create your FastAPI() instance, you can pass parameters that become the top-level metadata in the OpenAPI schema.
from fastapi import FastAPI
app = FastAPI(
title="My Amazing API",
description="An API for managing widgets.",
version="1.0.0",
contact={
"name": "Widget Support",
"email": "support@example.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
)
This sets the info section of your OpenAPI schema. Head to /openapi.json and you'll see info.title, info.version, info.description, info.contact, and info.license all filled in.
Step 2: Adding tags to endpoints
The simplest way to group endpoints is the tags parameter on a path operation decorator.
from fastapi import APIRouter, FastAPI
app = FastAPI()
widgets_router = APIRouter(prefix="/widgets", tags=["widgets"])
@widgets_router.get("/{widget_id}")
async def get_widget(widget_id: int):
"""Fetch a single widget."""
return {"widget_id": widget_id, "name": "Not implemented"}
app.include_router(widgets_router)
Now the GET /widgets/{widget_id} operation is tagged with widgets. The docs page will group it under a "widgets" section.
Step 3: Enriching operation descriptions
The next level is adding summaries and descriptions to each operation. These come from the function's docstring, or from the summary and description parameters.
@widgets_router.get(
"/{widget_id}",
summary="Get a widget by ID",
description="Returns the widget matching the given ID. Raises 404 if not found.",
response_description="The requested widget.",
)
async def get_widget(widget_id: int):
return {"widget_id": widget_id, "name": "Example widget"}
Step 4: Customizing the schema programmatically
If you need to change the generated schema in ways FastAPI doesn't expose directly, you can override the openapi method of your app.
from fastapi.openapi.utils import get_openapi
def custom_openapi(app: FastAPI):
# Call FastAPI's default function to get the base schema
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# Customize: add a custom top-level key
schema["x-custom-field"] = "This API is awesome"
# Override version if needed
schema["info"]["version"] = "2.0.0"
app.openapi_schema = schema
return app.openapi_schema
app.openapi = custom_openapi
Calling get_openapi ensures you still get all the standard generation logic. Then you can tweak the dict before caching it.
Hands-on walkthrough
Let's build a small but complete example that uses both tags and customizations. We'll create a mini API for a task manager.
First, set up your app with metadata and a couple of routers.
# main.py
from fastapi import FastAPI, APIRouter, HTTPException, status
from pydantic import BaseModel
app = FastAPI(
title="Task Manager API",
description="Manage your tasks with this simple API.",
version="1.0.0",
)
# Pydantic models
class Task(BaseModel):
id: int
title: str
done: bool = False
# In-memory storage
tasks_db = {}
# Router with tags
users_router = APIRouter(prefix="/tasks", tags=["tasks"])
@users_router.get("", summary="List all tasks", response_model=list[Task])
async def list_tasks():
return list(tasks_db.values())
@users_router.post(
"", summary="Create a task",
description="Creates a new task and returns it.",
response_model=Task,
status_code=status.HTTP_201_CREATED,
)
async def create_task(task: Task):
tasks_db[task.id] = task
return task
@users_router.get(
"/{task_id}",
summary="Get a task",
response_model=Task,
responses={404: {"description": "Task not found"}},
)
async def get_task(task_id: int):
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
return tasks_db[task_id]
app.include_router(users_router)
Now, run the app with Uvicorn: uvicorn main:app --reload. Visit /docs and you'll see a tasks section with all three endpoints nicely grouped. Each endpoint has a summary and description.
Let's go further: add an admin router with a different tag and a custom OpenAPI override.
# admin.py (or continue in main.py)
from fastapi import APIRouter
admin_router = APIRouter(prefix="/admin", tags=["admin"])
@admin_router.get("/stats", summary="Get system stats")
async def get_stats():
return {"total_tasks": len(tasks_db)}
# In main.py, after defining app and other routers:
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title="Task Manager API",
version="1.0.0",
description="Manage your tasks with this simple API.",
routes=app.routes,
)
schema["info"]["x-logo"] = {"url": "https://example.com/logo.png"}
schema["tags"] = [
{"name": "tasks", "description": "Operations on tasks."},
{"name": "admin", "description": "Administrative endpoints."},
]
app.openapi_schema = schema
return app.openapi_schema
app.openapi = custom_openapi
After restarting, check /openapi.json — you'll see info contains your custom field, and the tags array is populated with descriptions. FastAPI uses that tags array to add descriptions to the docs UI.
Now let's check the output of /openapi.json (truncated for brevity):
{
"openapi": "3.1.0",
"info": {
"title": "Task Manager API",
"version": "1.0.0",
"description": "Manage your tasks with this simple API.",
"x-logo": {"url": "https://example.com/logo.png"}
},
"paths": {
"/tasks": {
"get": {"tags": ["tasks"], "summary": "List all tasks"},
"post": {"tags": ["tasks"], "summary": "Create a task"}
},
"/tasks/{task_id}": {
"get": {"tags": ["tasks"], "summary": "Get a task"}
},
"/admin/stats": {
"get": {"tags": ["admin"], "summary": "Get system stats"}
}
},
"tags": [
{"name": "tasks", "description": "Operations on tasks."},
{"name": "admin", "description": "Administrative endpoints."}
]
}
You can see how the tags give structure and the custom field appears.
Compare options / when to choose what
You have several ways to apply tags and customizations. Here's when to use each:
| Approach | Best for | Example | Downside |
|---|---|---|---|
tags on decorator |
Simple grouping on one endpoint | @app.get("/items", tags=["items"]) |
Repetitive if many endpoints |
tags on APIRouter |
Grouping a whole router | router = APIRouter(tags=["items"]) |
All endpoints share same tag |
Overriding openapi |
Full schema control | def custom_openapi(): ... |
More code, need to preserve defaults |
Using description/summary on decorator |
Adding human explanation to one endpoint | @app.get("/x", summary="...", description="...") |
Verbose if many endpoints |
Decision guide:
- Use APIRouter tags as your default for clean grouping.
- Add summary and description on individual endpoints that need special explanation.
- Override openapi only for global tweaks like adding custom metadata, version overrides, or vendor extensions (keys starting with x-).
- Avoid mixing two tag styles without reason — it can confuse the docs UI.
Pro tip: If you're building an API for public consumption, invest time in a good
openapioverride — tools like Swagger UI and ReDoc both rely on the same schema, so improvements apply everywhere.
Troubleshooting & edge cases
- My tags aren't showing in
/docs– Did you passtagsto the decorator or router? Check that you didn't forget to include the router withapp.include_router(). Also verify you're not using a stale cached schema; restart your dev server or clear theopenapi_schemacache. /openapi.jsonis empty or missing my custom fields – If you overrideopenapi, make sure you setapp.openapi_schemaat the end on success. Also ensure you're not overwriting it later. Useprint(schema)for debugging.- The docs page shows endpoints at top level (not grouped) – You likely forgot to pass
tagsto the router and used@app.getdirectly. Addtags=["..."]to the decorator or router. get_openapiraisesValueError– This happens when you pass atitlethat's not a string orversionthat's not a string. Double-check your parameters. Also, if you call it with arouteslist that includes non-route objects, it can fail; passapp.routeswhich is already valid.- Upgrading FastAPI versions breaks my custom openapi – The signature of
get_openapimay change. Always check the official docs for your version. Adopt a best practice: wrap your customization in a function and guard against errors. - My Pydantic model doesn't appear in the schema – This usually happens if the model is only used as a type hint in a function without a response model. Add
response_model=Modelto the decorator.
What you learned & what's next
You now know how to build OpenAPI customizations and tags. Specifically, you can:
- Set API-level metadata (title, version, description, contact, license) on the
FastAPIinstance. - Group endpoints into logical sections using
tagson routers or individual decorators. - Enhance individual operations with
summary,description, andresponse_description. - Override the entire OpenAPI schema programmatically to add custom fields and tweak version info.
- Understand the relationship between your code and the generated
/openapi.json.
The next lesson in the FastAPI Backend Development track dives into handling file uploads and multipart forms — you'll see how to extend your API to handle binary data, another common real-world requirement. You'll use the skills you just learned to document those new endpoints properly.
Keep your API docs clean and your schema informative. Your future self and your API consumers will thank you.
Pro tip: Always commit your OpenAPI schema changes in code review — it's the contract your API exposes. A well-customized schema is a sign of a professional API.
Practice recap
Create a new FastAPI app with at least two routers (e.g., users and products). Add tags to each router, set a custom title and version, and write a short custom_openapi function that adds a vendor extension like x-docs-owner to the info section. Verify the changes in /docs and /openapi.json.
Common mistakes
- Forgetting to include the router after adding tags — the OpenAPI schema won't show the grouped endpoints, and the docs page remains flat.
- Overriding the
openapifunction but not caching the result inapp.openapi_schema— this can cause performance issues and unexpected rebuilds. - Using the same tag name inconsistently (e.g., 'task' vs 'tasks') — breaks grouping and confuses consumers.
Variations
- Use
fastapi.openapi.utils.get_openapito generate a base schema and then customize it — this is the most common approach. - Use a third-party library like
fastapi-versioningto add versioning to your OpenAPI schema without manual overrides. - Leverage FastAPI's built-in
openapi_tagsparameter on theFastAPIconstructor to provide tag descriptions directly, which avoids a customopenapioverride for simple cases.
Real-world use cases
- A public REST API for a SaaS product that needs clear grouping for documentation portals and SDK generation.
- An internal microservice that must expose custom metadata like compliance tags in the OpenAPI schema for governance tools.
- A B2B API that uses vendor extensions (
x-*) to embed usage limits and rate-limit info for client consumers.
Key takeaways
- Tags group endpoints into logical sections on the docs page — apply them on routers for DRY code.
- API-level metadata (title, version, description) is set via the
FastAPI()constructor. - Each operation can be enriched with
summary,description, andresponse_description. - Override the
openapimethod to programmatically customize the generated schema, but always fall back toget_openapidefaults. - The OpenAPI schema is a Python dict — any customization is limited only by your creativity.
- A clean, well-documented schema is a professional asset for your API.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.