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:
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:
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:
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:
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:
@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:
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:
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:
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:
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:
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:
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:
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:
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:
- Access the generated OpenAPI schema via
/openapi.json
- Customize API metadata like title, description, and version
- Organize API endpoints using tags
- Add detailed descriptions to parameters, request bodies, and responses
- Provide examples to make your documentation more user-friendly
- 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
- FastAPI Official Documentation on OpenAPI
- OpenAPI Specification
- Swagger UI
- ReDoc Documentation Generator
Exercises
- Create a FastAPI application with at least three endpoints and organize them using tags.
- Add examples and detailed descriptions to all parameters and response models.
- Customize the metadata of your API with title, description, and version.
- Create a Pydantic model with custom examples and field descriptions.
- 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! :)