OpenAPI, Swagger, ReDoc, кастомизация документации
FastAPI автоматически генерирует OpenAPI документацию. В этой теме вы научитесь кастомизировать Swagger UI, ReDoc и создавать подробную документацию для разработчиков.
FastAPI автоматически генерирует документацию на основе:
| Endpoint | Описание |
|---|---|
/docs | Swagger UI (интерактивная) |
/redoc | ReDoc (статичная, для чтения) |
/openapi.json | OpenAPI схема (JSON) |
from fastapi import FastAPI
app = FastAPI(
title="My API",
description="Полное описание моего API",
version="1.0.0",
terms_of_service="https://example.com/terms/",
contact={
"name": "Support",
"email": "support@example.com",
"url": "https://example.com/support",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
docs_url="/docs", # Swagger UI
redoc_url="/redoc", # ReDoc
openapi_url="/openapi.json"
)Что здесь происходит:
title, description, version — базовая информация, отображается в Swagger UI и ReDocterms_of_service, contact, license_info — метаданные API (опционально, но полезно для публичных API)docs_url, redoc_url, openapi_url — кастомизация путей к документации (можно отключить, установив None)Результат:
При открытии /docs вы увидите заголовок "My API" и описание в верхней части страницы. В ReDoc (/redoc) эта же информация отобразится в шапке.
## Документирование endpoints
### Docstring и описания
```python
from fastapi import FastAPI, Query
app = FastAPI()
@app.get(
"/users",
summary="Получить список пользователей",
description="""
Получает список пользователей с поддержкой пагинации.
## Возможности
- **Пагинация**: используйте параметры skip и limit
- **Сортировка**: по имени или дате создания
- **Фильтрация**: по статусу (active/inactive)
## Пример ответа
```json
{
"users": [...],
"total": 100,
"page": 1
}
""", response_description="Список пользователей" ) def get_users( skip: int = Query(0, ge=0, description="Пропустить N пользователей"), limit: int = Query(10, ge=1, le=100, description="Вернуть не более N пользователей"), status: str = Query(None, description="Фильтр по статусу: active или inactive") ): """ Получает пользователей из базы данных.
# Алгоритм
1. Применяем фильтры
2. Сортируем по дате создания
3. Возвращаем страницу
"""
...
**Что здесь происходит:**
- `summary` — короткий заголовок endpoint (отображается в Swagger UI как название операции)
- `description` — развёрнутое описание с поддержкой Markdown. Отображается при раскрытии endpoint в Swagger UI
- `response_description` — описание того, что возвращает endpoint (по умолчанию "Successful Response")
- Docstring функции — используется как fallback для `description`, если параметр `description` не задан
- `Query(..., description="...")` — описание каждого query-параметра, отображается в Swagger UI при наведении или в форме для тестирования
**Результат в Swagger UI:**
При открытии `/docs` вы увидите endpoint GET /users с заголовком "Получить список пользователей". При раскрытии — описание с Markdown-форматированием и поля для ввода skip, limit, status с подсказками.
> **Совет**: `description` в Query/Path/Body обязателен для хорошей документации. Без него пользователь не поймёт, что означает параметр.
from pydantic import BaseModel, Field, EmailStr
from typing import Literal
class UserCreate(BaseModel):
"""
Модель для создания пользователя.
Используется в endpoint POST /users
"""
username: str = Field(
...,
min_length=3,
max_length=50,
description="Имя пользователя (3-50 символов, только буквы и цифры)",
example="john_doe"
)
email: EmailStr = Field(
...,
description="Email адрес пользователя",
example="john@example.com"
)
password: str = Field(
...,
min_length=8,
description="Пароль (минимум 8 символов)",
example="SecurePass123"
)
role: Literal["user", "admin"] = Field(
default="user",
description="Роль пользователя"
)
class UserResponse(BaseModel):
"""
Модель ответа с данными пользователя.
"""
id: int = Field(..., description="Уникальный ID пользователя", example=1)
username: str = Field(..., description="Имя пользователя", example="john_doe")
email: EmailStr = Field(..., description="Email адрес", example="john@example.com")
role: str = Field(..., description="Роль пользователя", example="user")
created_at: str = Field(..., description="Дата создания", example="2024-01-01T12:00:00")
model_config = {
"json_schema_extra": {
"example": {
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"role": "user",
"created_at": "2024-01-01T12:00:00"
}
}
}Что здесь происходит:
Field(...) — ... (Ellipsis) означает обязательное поле. Без него клиент получит ошибку валидацииField(default=...) — поле с значением по умолчанию (не обязательно при создании)description — описание поля, отображается в Swagger UI и ReDoc в схеме моделиexample — пример значения для этого поля, используется Swagger UI для автозаполнения формыmin_length, max_length — валидация строки + автоматическая документация ограниченийmodel_config с json_schema_extra — пример всего объекта ответа (не отдельных полей). Отображается в Swagger UI как "Example Value" в секции ResponsesРазница между example в Field и json_schema_extra:
example в Field — пример для одного поля (используется в схеме запроса)json_schema_extra["example"] — пример всего ответа (отображается в Responses)Совет: всегда указывайте
example— это позволяет пользователям Swagger UI протестировать API одним кликом (кнопка "Try it out" подставит значения).
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
app = FastAPI()
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url="/openapi.json",
title="My API - Docs",
swagger_ui_parameters={
"defaultModelsExpandDepth": -1, # Скрыть модели по умолчанию
"defaultModelExpandDepth": 2, # Глубина раскрытия модели
"docExpansion": "list", # 'none', 'list', 'full'
"filter": True, # Поиск по endpoints
"showExtensions": True, # Показывать расширения
"syntaxHighlight.theme": "monokai", # Тема подсветки
}
)Что здесь происходит:
/docs endpoint, чтобы передать кастомные параметрыinclude_in_schema=False — сам endpoint /docs не попадёт в документацию (иначе будет рекурсия)openapi_url="/openapi.json" — указываем, откуда Swagger UI берёт схему (обязательно)Параметры Swagger UI:
defaultModelsExpandDepth: -1 — скрыть секцию "Schemas" (модели) по умолчанию. Полезно, если моделей много и они загромождают интерфейсdocExpansion: "list" — показывает только пути и методы, без описаний. Варианты: "none" (свёрнуто), "list" (пути раскрыты), "full" (всё раскрыто)filter: True — добавляет строку поиска вверху Swagger UI. Можно фильтровать endpoints по названию или тегуsyntaxHighlight.theme — тема подсветки синтаксиса. Доступны: "monokai", "agate", "arta", "github" и другиеРезультат: Swagger UI будет выглядеть иначе: строка поиска вверху, модели скрыты, подсветка кода в теме Monokai.
from fastapi import FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI(
openapi_tags=[
{
"name": "users",
"description": "Операции с пользователями",
},
{
"name": "posts",
"description": "Операции с постами",
}
]
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token", tags=["auth"])
async def login():
"""Получить токен доступа"""
...
@app.get("/users/me", tags=["users"])
async def read_users_me():
"""Получить текущего пользователя"""
...Что здесь происходит:
OAuth2PasswordBearer(tokenUrl="token") — сообщает FastAPI, что API использует OAuth2. Swagger UI добавит кнопку "Authorize" вверху страницыtokenUrl="token" — endpoint для получения токена (POST /token). В Swagger UI при нажатии "Authorize" появится форма с полями username/passwordopenapi_tags — группировка endpoints по тегам. В Swagger UI теги отображаются как заголовки секцийtags=["users"] в декораторе — указывает, к какой группе относится endpoint. Один endpoint может иметь несколько тегов: tags=["users", "admin"]Как это работает в Swagger UI:
/token, получение JWTAuthorization: Bearer <token> для всех защищённых endpointsВажно:
OAuth2PasswordBearerтолько добавляет UI для авторизации в Swagger. Реальную проверку токена нужно реализовать черезDepends(oauth2_scheme)в защищённых endpoints.
## Кастомизация ReDoc
```python
from fastapi import FastAPI
from fastapi.openapi.docs import get_redoc_html
app = FastAPI()
@app.get("/redoc", include_in_schema=False)
async def custom_redoc_html():
return get_redoc_html(
openapi_url="/openapi.json",
title="My API - ReDoc",
redoc_js_url="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
redoc_js_settings={
"theme": {
"colors": {
"primary": {
"main": "#1976d2"
}
}
},
"hideHostname": True,
"hideDownloadButton": True,
"requiredPropsFirst": True
}
)
Что здесь происходит:
/redoc endpoint для кастомизацииredoc_js_url — URL к JavaScript бандлу ReDoc. Можно использовать CDN или захостить локальноredoc_js_settings — настройки внешнего вида и поведения ReDocПараметры ReDoc:
theme.colors.primary.main — основной цвет заголовков и ссылок (брендирование)hideHostname: True — скрывает базовый URL сервера (меньше шума, если домен очевиден)hideDownloadButton: True — скрывает кнопку скачивания OpenAPI схемы (полезно для внутренних API)requiredPropsFirst: True — обязательные поля отображаются первыми в схеме (удобнее для чтения)ReDoc vs Swagger UI:
from fastapi import FastAPI, APIRouter
app = FastAPI(
openapi_tags=[
{
"name": "users",
"description": "Управление пользователями",
"externalDocs": {
"url": "https://example.com/docs/users"
}
},
{
"name": "posts",
"description": "Управление постами"
},
{
"name": "auth",
"description": "Аутентификация и авторизация"
}
]
)
# Роутер с тегами
users_router = APIRouter(tags=["users"])
@users_router.get("/users")
def get_users():
...
@users_router.post("/users")
def create_user():
...
app.include_router(users_router)
# Отдельные теги для endpoint
@app.post("/login", tags=["auth"])
def login():
...Что здесь происходит:
openapi_tags — глобальное определение тегов с описаниями. Порядок в списке = порядок отображения в Swagger UIexternalDocs — ссылка на внешнюю документацию (например, Notion, Confluence, GitHub Wiki)APIRouter(tags=["users"]) — все endpoints в этом роутере автоматически получают тег userstags=["auth"] в декораторе — явное указание тега для отдельного endpoint (если не используется роутер с тегами)Зачем группировать:
Результат в Swagger UI:
▼ auth
POST /login — Аутентификация и авторизация
▼ users
GET /users — Управление пользователями
POST /users
▼ posts
GET /posts — Управление постами
POST /posts
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(..., example="Laptop")
price: float = Field(..., example=999.99)
description: str | None = Field(None, example="Gaming laptop")
@app.post(
"/items",
response_model=Item,
responses={
200: {
"description": "Успешное создание",
"content": {
"application/json": {
"example": {
"name": "Laptop",
"price": 999.99,
"description": "Gaming laptop"
}
}
}
},
400: {
"description": "Ошибка валидации",
"content": {
"application/json": {
"example": {
"detail": "Invalid input"
}
}
}
},
422: {
"description": "Ошибка валидации данных"
}
}
)
def create_item(item: Item):
"""
Создать новый элемент.
## Пример запроса
```json
{
"name": "Laptop",
"price": 999.99
}
```
"""
return itemЧто здесь происходит:
response_model=Item — указывает FastAPI, какую модель использовать для ответа. Автоматически генерируется схема в OpenAPIresponses — кастомизация ответов для разных HTTP статусов. Без этого параметра FastAPI документирует только успешный ответ (200) и ошибки валидации (422)responses[200] — пример успешного ответа. Переопределяет автогенерированный пример из response_modelresponses[400] — документирование кастомной ошибки (например, бизнес-логика: "нельзя создать товар с отрицательной ценой")responses[422] — стандартная ошибка валидации Pydantic (обычно не требует примера, т.к. формат стандартный)Зачем указывать responses:
Совет: документируйте как минимум 200, 401 (если нужна авторизация), и 422. Кастомные ошибки (400, 404, 409) — по необходимости.
from fastapi import FastAPI
app = FastAPI()
@app.get("/internal", include_in_schema=False)
def internal_endpoint():
"""Этот endpoint не будет в документации"""
return {"status": "internal"}
@app.get("/public")
def public_endpoint():
"""Этот endpoint будет в документации"""
return {"status": "public"}Что здесь происходит:
include_in_schema=False — полностью скрывает endpoint из OpenAPI схемы, Swagger UI и ReDocКогда скрывать endpoints:
/health) — не нужен клиентам API/metrics) — только для мониторинговых систем/internal/*) — для внутренних сервисов/debug/*) — не должны быть видны в productionВажно: скрытый endpoint не означает защищённый! Если нужно ограничить доступ — используйте авторизацию, а не скрытие из документации.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root():
return """
<!DOCTYPE html>
<html>
<head>
<title>My API Documentation</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
.links { margin: 20px 0; }
.links a { display: inline-block; margin-right: 20px; padding: 10px 20px;
background: #007bff; color: white; text-decoration: none; border-radius: 5px; }
.links a:hover { background: #0056b3; }
</style>
</head>
<body>
<h1>My API Documentation</h1>
<p>Добро пожаловать в документацию My API!</p>
<div class="links">
<a href="/docs">Swagger UI</a>
<a href="/redoc">ReDoc</a>
<a href="/openapi.json">OpenAPI Schema</a>
</div>
<h2>Quick Start</h2>
<pre><code>
# Получить токен
curl -X POST http://localhost:8000/token \\
-d "username=john&password=secret"
# Использовать токен
curl http://localhost:8000/users/me \\
-H "Authorization: Bearer YOUR_TOKEN"
</code></pre>
<h2>Authentication</h2>
<p>API использует OAuth2 с JWT токенами.</p>
<h2>Rate Limits</h2>
<ul>
<li>100 запросов в минуту для аутентифицированных</li>
<li>10 запросов в минуту для анонимных</li>
</ul>
</body>
</html>
"""Что здесь происходит:
/) вместо стандартного 404 или JSON ответаresponse_class=HTMLResponse — указывает FastAPI вернуть HTML, а не JSONЗачем создавать кастомную страницу:
http://localhost:8000 и видит инструкции, а не {"detail": "Not Found"}Альтернатива: для серьёзных проектов используйте MkDocs, Docusaurus или Sphinx для отдельного сайта документации, а не HTML внутри кода.
from fastapi import FastAPI
from fastapi.responses import Response
import json
import yaml
app = FastAPI()
# ... endpoints ...
# Экспорт схемы
@app.get("/export/openapi.json", include_in_schema=False)
def export_openapi():
return app.openapi()
@app.get("/export/openapi.yaml", include_in_schema=False)
def export_openapi_yaml():
openapi_schema = app.openapi()
return Response(
content=yaml.dump(openapi_schema),
media_type="application/x-yaml"
)Что здесь происходит:
app.openapi() — возвращает полную OpenAPI 3.0 схему как Python dictЗачем экспортировать схему:
Совет: для CI/CD можно сохранить схему в артефакт:
curl http://localhost:8000/openapi.json > openapi.json
# Python клиент
openapi-generator generate -i http://localhost:8000/openapi.json -g python -o ./client
# TypeScript клиент
openapi-generator generate -i http://localhost:8000/openapi.json -g typescript-axios -o ./client
# Java клиент
openapi-generator generate -i http://localhost:8000/openapi.json -g java -o ./clientЧто здесь происходит:
openapi-generator — утилита командной строки для генерации кода из OpenAPI схемы-i — входной файл (URL или локальный путь)-g — генератор (язык/фреймворк). Полный список: openapi-generator list-o — выходная директорияУстановка:
# macOS
brew install openapi-generator
# Linux/Windows (через npm)
npm install @openapitools/openapi-generator-cli -g
# Docker
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate ...Что генерируется:
Зачем генерировать клиентов:
Совет: добавьте генерацию в CI/CD пайелайн, чтобы клиенты всегда были в sync с API.
from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional
app = FastAPI(
title="Blog API",
description="""
## Blog API - Полнофункциональное API для блога
### Возможности
- **Пользователи**: регистрация, аутентификация, профиль
- **Посты**: создание, редактирование, удаление, пагинация
- **Комментарии**: добавление комментариев к постам
- **Теги**: категоризация постов
### Аутентификация
API использует OAuth2 с JWT токенами.
Получите токен через `/token` и используйте в заголовке `Authorization: Bearer <token>`.
### Rate Limits
- 100 запросов/мин для аутентифицированных
- 10 запросов/мин для анонимных
""",
version="2.0.0",
contact={
"name": "API Support",
"email": "api-support@example.com",
"url": "https://example.com/support"
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
)
# === Models ===
class PostCreate(BaseModel):
"""Модель для создания поста"""
title: str = Field(
...,
min_length=5,
max_length=200,
description="Заголовок поста",
example="Как изучить FastAPI"
)
content: str = Field(
...,
min_length=10,
description="Содержимое поста",
example="FastAPI — современный фреймворк..."
)
tags: List[str] = Field(
default=[],
description="Теги поста",
example=["python", "fastapi", "tutorial"]
)
class PostResponse(BaseModel):
"""Модель ответа с постом"""
id: int = Field(..., description="ID поста", example=1)
title: str = Field(..., description="Заголовок", example="Как изучить FastAPI")
content: str = Field(..., description="Содержимое")
author_id: int = Field(..., description="ID автора", example=1)
tags: List[str] = Field(..., description="Теги")
created_at: str = Field(..., description="Дата создания", example="2024-01-01T12:00:00")
# === Endpoints ===
@app.post(
"/posts",
response_model=PostResponse,
status_code=status.HTTP_201_CREATED,
tags=["posts"],
summary="Создать пост",
description="Создаёт новый пост в блоге",
response_description="Созданный пост"
)
async def create_post(post: PostCreate):
"""
Создаёт новый пост.
## Требования
- Аутентификация обязательна
- Заголовок: 5-200 символов
- Содержимое: минимум 10 символов
## Пример
```json
{
"title": "Как изучить FastAPI",
"content": "FastAPI — современный...",
"tags": ["python", "fastapi"]
}
```
"""
...
@app.get(
"/posts",
response_model=List[PostResponse],
tags=["posts"],
summary="Получить список постов"
)
async def list_posts(
skip: int = Field(0, ge=0, description="Пропустить N постов"),
limit: int = Field(10, ge=1, le=100, description="Вернуть не более N постов"),
tag: Optional[str] = Field(None, description="Фильтр по тегу")
):
"""
Получает список постов с пагинацией.
"""
...Что здесь происходит:
description на уровне приложения описывает возможности, аутентификацию, rate limitsField(..., description, example) — каждый параметр документированsummary, description, tags, response_modelРезультат:
/docs) — интерактивная документация с возможностью тестирования/redoc) — красивая статичная страница для чтения/openapi.json) — для генерации клиентов и интеграцийЧеклист хорошей документации:
title, description, version в FastAPI()summary и description для каждого endpointdescription и example для каждого поля в моделяхresponses с примерами ошибок (401, 404, 400)tags для группировки endpointsopenapi_tags с описаниями групп/ (опционально)В следующей теме вы изучите Docker и деплой — контейнеризация, docker-compose, production deployment.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.