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

Sponsored

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 openapi override — 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 pass tags to the decorator or router? Check that you didn't forget to include the router with app.include_router(). Also verify you're not using a stale cached schema; restart your dev server or clear the openapi_schema cache.
  • /openapi.json is empty or missing my custom fields – If you override openapi, make sure you set app.openapi_schema at the end on success. Also ensure you're not overwriting it later. Use print(schema) for debugging.
  • The docs page shows endpoints at top level (not grouped) – You likely forgot to pass tags to the router and used @app.get directly. Add tags=["..."] to the decorator or router.
  • get_openapi raises ValueError – This happens when you pass a title that's not a string or version that's not a string. Double-check your parameters. Also, if you call it with a routes list that includes non-route objects, it can fail; pass app.routes which is already valid.
  • Upgrading FastAPI versions breaks my custom openapi – The signature of get_openapi may 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=Model to 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 FastAPI instance.
  • Group endpoints into logical sections using tags on routers or individual decorators.
  • Enhance individual operations with summary, description, and response_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 openapi function but not caching the result in app.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

  1. Use fastapi.openapi.utils.get_openapi to generate a base schema and then customize it — this is the most common approach.
  2. Use a third-party library like fastapi-versioning to add versioning to your OpenAPI schema without manual overrides.
  3. Leverage FastAPI's built-in openapi_tags parameter on the FastAPI constructor to provide tag descriptions directly, which avoids a custom openapi override 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, and response_description.
  • Override the openapi method to programmatically customize the generated schema, but always fall back to get_openapi defaults.
  • 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.