Skip to main content

FastAPI OpenAPI Schema

FastAPI provides automatic documentation for your API based on the OpenAPI standard. In this guide, you'll learn what the OpenAPI schema is, how FastAPI generates it, and how you can customize it to better document your API.

Introduction to OpenAPI Schema

OpenAPI (formerly known as Swagger) is a specification for machine-readable API descriptions. When you build an API with FastAPI, it automatically generates an OpenAPI schema that documents all your API endpoints, request bodies, responses, parameters, and more.

This schema powers:

  • Interactive API documentation (Swagger UI)
  • Alternative API documentation (ReDoc)
  • Client code generation tools
  • Testing tools

How FastAPI Generates the Schema

FastAPI automatically generates the OpenAPI schema by examining:

  • Function parameters
  • Type annotations
  • Pydantic models
  • Path operations (routes)
  • Dependencies

Let's see how to create a basic API and access its OpenAPI schema:

python
from fastapi import FastAPI

app = FastAPI(
title="My Super API",
description="This is a sample API for demonstration",
version="0.1.0"
)

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
"""
Fetch an item by its ID.

- **item_id**: The ID of the item to retrieve
- **q**: Optional query parameter
"""
return {"item_id": item_id, "q": q}

When you run this application with Uvicorn:

bash
uvicorn main:app --reload

You can access:

  • The OpenAPI JSON schema at /openapi.json
  • Swagger UI at /docs
  • ReDoc at /redoc

Customizing the OpenAPI Schema

API Metadata

You can customize general information about your API when initializing the FastAPI application:

python
from fastapi import FastAPI

app = FastAPI(
title="My Inventory API",
description="API for managing inventory items in a warehouse",
version="1.0.0",
terms_of_service="http://example.com/terms/",
contact={
"name": "API Support Team",
"url": "http://example.com/contact/",
"email": "[email protected]",
},
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
},
)

Tags

Tags help organize your API endpoints into logical groups:

python
from fastapi import FastAPI, APIRouter

app = FastAPI()

# Create routers with tags
router_users = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}},
)

router_items = APIRouter(
prefix="/items",
tags=["items"],
responses={404: {"description": "Not found"}},
)

@router_users.get("/")
async def list_users():
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

@router_items.get("/")
async def list_items():
return [{"id": 1, "name": "Hammer"}, {"id": 2, "name": "Screwdriver"}]

# Add routers to app
app.include_router(router_users)
app.include_router(router_items)

Alternatively, you can add tags to individual path operations:

python
@app.get("/users/", tags=["users"])
async def list_users():
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

@app.get("/items/", tags=["items"])
async def list_items():
return [{"id": 1, "name": "Hammer"}, {"id": 2, "name": "Screwdriver"}]

Response Descriptions

You can specify response models and descriptions:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Union

app = FastAPI()

class Item(BaseModel):
id: int
name: str
description: Union[str, None] = None
price: float

class ErrorResponse(BaseModel):
message: str

@app.get(
"/items/{item_id}",
response_model=Item,
responses={
200: {
"description": "Item found successfully",
"model": Item,
},
404: {
"description": "Item not found",
"model": ErrorResponse,
},
},
)
async def read_item(item_id: int):
if item_id == 42:
return {
"id": item_id,
"name": "Magic item",
"description": "This item has magical properties",
"price": 99.99
}
raise HTTPException(status_code=404, detail="Item not found")

Path Parameter Documentation

Path parameters can be better documented with more metadata:

python
from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(
item_id: int = Path(
...,
title="Item ID",
description="The unique identifier of the item to retrieve",
ge=1, # greater than or equal to 1
),
):
return {"item_id": item_id}

Query Parameter Documentation

Similarly, we can enhance query parameter documentation:

python
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search/")
async def search_items(
q: str = Query(
None,
title="Search Query",
description="Search items by keyword",
min_length=3,
max_length=50,
example="hammer",
),
skip: int = Query(
0,
title="Skip",
description="Number of items to skip",
ge=0,
),
limit: int = Query(
10,
title="Limit",
description="Maximum number of items to return",
le=100,
),
):
return {
"query": q,
"skip": skip,
"limit": limit,
"results": [f"{q} item {i}" for i in range(skip, skip + limit)]
}

Advanced Schema Customization

Field Examples

Adding examples makes your documentation more user-friendly:

python
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Union

app = FastAPI()

class Item(BaseModel):
name: str = Field(..., example="Screwdriver")
description: Union[str, None] = Field(None, example="A flat-head screwdriver")
price: float = Field(..., example=9.99)
tax: Union[float, None] = Field(None, example=1.23)

@app.post("/items/")
async def create_item(item: Item):
return item

Request Body Examples

You can provide examples for entire request bodies:

python
from fastapi import FastAPI, Body
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None

@app.post("/items/")
async def create_item(
item: Item = Body(
...,
examples={
"normal": {
"summary": "A normal item",
"description": "A normal item with all required fields",
"value": {
"name": "Screwdriver",
"description": "A basic tool",
"price": 9.99,
"tax": 0.80
}
},
"minimal": {
"summary": "A minimal item",
"description": "A minimal item with only required fields",
"value": {
"name": "Screwdriver",
"price": 9.99
}
},
}
),
):
return item

Model Config for Documentation

You can configure documentation for your Pydantic models:

python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
name: str = Field(..., description="The name of the item")
description: str = Field(None, description="Optional detailed description")
price: float = Field(..., description="Current price in USD")
tax: float = Field(None, description="Tax rate applied to the item")

class Config:
schema_extra = {
"example": {
"name": "Hammer",
"description": "A sturdy hammer with steel head",
"price": 12.50,
"tax": 0.95
}
}

@app.post("/items/")
async def create_item(item: Item):
return item

Disabling the OpenAPI Schema

In some cases, you might want to disable the automatic generation of documentation:

python
from fastapi import FastAPI

# Disable OpenAPI documentation
app = FastAPI(openapi_url=None)

@app.get("/items/")
async def read_items():
return [{"name": "Item 1"}, {"name": "Item 2"}]

This will disable the /openapi.json endpoint and, consequently, both the Swagger UI and ReDoc interfaces.

Real-World Example: E-commerce API

Let's create a more complete example for an e-commerce API with better documentation:

python
from fastapi import FastAPI, Path, Query, Body, HTTPException
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum

app = FastAPI(
title="E-commerce API",
description="API for an online store with products, categories, and orders",
version="1.0.0",
)

class CategoryEnum(str, Enum):
electronics = "electronics"
clothing = "clothing"
books = "books"
home = "home"
sports = "sports"

class ProductBase(BaseModel):
name: str = Field(..., description="Name of the product", example="Wireless Headphones")
description: str = Field(..., description="Detailed description", example="Noise-cancelling wireless headphones")
price: float = Field(..., gt=0, description="Price in USD", example=99.99)
category: CategoryEnum = Field(..., description="Product category", example="electronics")
in_stock: bool = Field(True, description="Availability status")

class Config:
schema_extra = {
"example": {
"name": "Wireless Headphones",
"description": "Premium noise-cancelling wireless headphones",
"price": 99.99,
"category": "electronics",
"in_stock": True
}
}

class ProductCreate(ProductBase):
pass

class Product(ProductBase):
id: int = Field(..., description="Unique product identifier")

@app.post("/products/",
response_model=Product,
status_code=201,
tags=["products"],
summary="Create a new product",
description="Create a new product with the provided details"
)
async def create_product(product: ProductCreate):
"""
Create a new product in the catalog:

- **name**: Product name
- **description**: Detailed product description
- **price**: Product price in USD
- **category**: Product category
- **in_stock**: Availability status

Returns the created product with its assigned ID.
"""
# In a real application, we would save to a database
new_product = Product(id=1, **product.dict())
return new_product

@app.get("/products/",
response_model=List[Product],
tags=["products"],
summary="List all products",
description="Get a list of all products, with optional filtering"
)
async def list_products(
category: Optional[CategoryEnum] = Query(
None,
description="Filter by category"
),
min_price: Optional[float] = Query(
None,
ge=0,
description="Minimum price filter"
),
max_price: Optional[float] = Query(
None,
ge=0,
description="Maximum price filter"
),
in_stock: Optional[bool] = Query(
None,
description="Filter by availability"
),
):
"""
Get all products with optional filters:

- **category**: Filter by product category
- **min_price**: Filter by minimum price
- **max_price**: Filter by maximum price
- **in_stock**: Filter by availability

Returns a list of products that match the criteria.
"""
# In a real application, we would query a database with these filters
products = [
Product(
id=1,
name="Wireless Headphones",
description="Premium noise-cancelling wireless headphones",
price=99.99,
category=CategoryEnum.electronics,
in_stock=True
),
Product(
id=2,
name="Running Shoes",
description="Comfortable running shoes with good support",
price=79.99,
category=CategoryEnum.sports,
in_stock=True
)
]

# Apply filters (simplified example)
if category:
products = [p for p in products if p.category == category]
if min_price is not None:
products = [p for p in products if p.price >= min_price]
if max_price is not None:
products = [p for p in products if p.price <= max_price]
if in_stock is not None:
products = [p for p in products if p.in_stock == in_stock]

return products

@app.get("/products/{product_id}",
response_model=Product,
tags=["products"],
summary="Get a specific product",
description="Get detailed information about a specific product by its ID",
responses={
200: {
"description": "The requested product",
"content": {
"application/json": {
"example": {
"id": 1,
"name": "Wireless Headphones",
"description": "Premium noise-cancelling wireless headphones",
"price": 99.99,
"category": "electronics",
"in_stock": True
}
}
}
},
404: {
"description": "Product not found",
"content": {
"application/json": {
"example": {"detail": "Product with ID 999 not found"}
}
}
}
}
)
async def get_product(
product_id: int = Path(
...,
title="Product ID",
description="The unique identifier of the product",
ge=1
)
):
"""
Get a specific product by its ID:

- **product_id**: The unique identifier of the product

Returns detailed information about the requested product.
"""
# In a real application, we would query a database
if product_id == 1:
return Product(
id=1,
name="Wireless Headphones",
description="Premium noise-cancelling wireless headphones",
price=99.99,
category=CategoryEnum.electronics,
in_stock=True
)
raise HTTPException(status_code=404, detail=f"Product with ID {product_id} not found")

Summary

FastAPI's automatic OpenAPI schema generation makes it easy to create well-documented APIs. You've learned how to:

  1. Access the generated OpenAPI schema via /openapi.json
  2. Customize API metadata like title, description, and version
  3. Organize API endpoints using tags
  4. Add detailed descriptions to parameters, request bodies, and responses
  5. Provide examples to make your documentation more user-friendly
  6. Configure advanced schema options for better documentation

By following these practices, you can create APIs that are not only functional but also well-documented, making them easier for other developers to use.

Additional Resources

Exercises

  1. Create a FastAPI application with at least three endpoints and organize them using tags.
  2. Add examples and detailed descriptions to all parameters and response models.
  3. Customize the metadata of your API with title, description, and version.
  4. Create a Pydantic model with custom examples and field descriptions.
  5. Implement path operations with different response codes and appropriate documentation.

By mastering OpenAPI schema customization, you'll be able to create APIs that are both powerful and easy to use!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)