Django DRF #5 OpenAPI ドキュメント — drf-spectacular
#1 ~ #4 で作った API が豊かになりました。次は フロントとどうやって約束を合わせるか が次のテーマ。答えは OpenAPI 自動ドキュメント。
自動ドキュメントがなぜ価値があるか #
フロントエンド / モバイルとの協業で「このエンドポイントのレスポンス構造を教えてください」が毎回繰り返されるやり取りが自動ドキュメントで消えます。
- 契約 (contract) — Swagger UI が真実。Slack / Notion ドキュメントが真実ではない
- クライアント SDK の自動生成 — TypeScript / Swift / Kotlin SDK を OpenAPI 仕様で生成
- テスト — schema diff で breaking change を検知
- 外部公開 — 外部開発者に公開しやすい形式
- オンボーディング — 新規メンバーが API 全体を一目で
コードとドキュメントがずれることがありません — 関数シグネチャがドキュメント になるからです。
drf-spectacular vs drf-yasg #
DRF 陣営の 2 つの分かれ道。
| drf-yasg | drf-spectacular | |
|---|---|---|
| OpenAPI バージョン | 2.0 (Swagger) | 3.x |
| アクティブ度 | メンテナンスモード | 活発 |
| 型ヒント推論 | 普通 | 強い |
| 推奨 (現在) | — | 新規はこちら |
OpenAPI 3.x が事実上の標準になった今、新規プロジェクトは drf-spectacular が最初の選択。この記事も spectacular ベースです。
インストール #
uv add drf-spectacularINSTALLED_APPS += ["drf_spectacular"]
REST_FRAMEWORK = {
...,
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Blog API",
"DESCRIPTION": "Django DRFシリーズの例ブログ API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False, # 別途の schema URL のみ公開
"COMPONENT_SPLIT_REQUEST": True, # request/response コンポーネントを分離
"SCHEMA_PATH_PREFIX": "/api/",
"SERVERS": [
{"url": "https://api.example.com", "description": "Production"},
{"url": "http://localhost:8000", "description": "Local"},
],
"TAGS": [
{"name": "posts", "description": "記事 CRUD"},
{"name": "comments", "description": "コメント"},
{"name": "auth", "description": "認証"},
],
}URL ルート #
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
SpectacularRedocView,
)
urlpatterns = [
...,
# 原本の OpenAPI スキーマ (JSON / YAML)
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
# 人向けの UI
path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
]これで終わりです。ブラウザで確認:
| URL | 何 |
|---|---|
/api/schema/ | OpenAPI 3.x JSON (または ?format=openapi-json/-yaml) |
/api/docs/ | Swagger UI — Try it out が可能 |
/api/redoc/ | ReDoc スタイル (読み取り中心のすっきりしたドキュメント) |
CLI で抽出 #
CI 段階で schema ファイルを出力したいなら。
uv run python manage.py spectacular --file schema.yml --validate--validate が OpenAPI 3.x 仕様に違反しないかをチェック。CI でこれがパスするのが最初のゲート。
自動推論 — 何が無料で得られるか #
drf-spectacular は ViewSet/Serializer だけで 既に豊かなスキーマ を作ってくれます。
| 出処 | スキーマに変換 |
|---|---|
serializer_class | request / response body |
queryset.model | response のオブジェクト型 |
filter_backends、filterset_class | クエリパラメータ (フィルタ) |
ordering_fields、search_fields | ?ordering=、?search= パラメータ |
pagination_class | レスポンス envelope (count/next/previous) |
permission_classes | 認証要否、401 レスポンス |
URL の {pk} | path パラメータ |
@action(detail=True/False) | 追加ルート |
#1 ~ #3 で作ったコードをそのまま置けば、Swagger UI には既に:
/api/posts/GET (list、ページネーション、フィルタ / ソート / 検索パラメータ)/api/posts/POST (PostSerializer 入力、201 レスポンス)/api/posts/{id}/GET/PUT/PATCH/DELETE/api/posts/{id}/publish/POST- 401 / 403 レスポンス表示 (perm クラス基盤)
まで全部入っています。
@extend_schema — 精密な明細
#
自動推論が拾えない部分 — レスポンス例、複数レスポンス、追加パラメータ、エラースキーマ — はデコレータで埋めます。
from drf_spectacular.utils import (
extend_schema,
extend_schema_view,
OpenApiParameter,
OpenApiResponse,
OpenApiExample,
inline_serializer,
)
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
@extend_schema_view(
list=extend_schema(
summary="記事一覧",
description="公開記事 + 自分の記事をページネーションで返します。",
tags=["posts"],
),
retrieve=extend_schema(summary="記事詳細", tags=["posts"]),
create=extend_schema(summary="記事作成", tags=["posts"]),
update=extend_schema(summary="記事の全体修正", tags=["posts"]),
partial_update=extend_schema(summary="記事の部分修正", tags=["posts"]),
destroy=extend_schema(summary="記事削除", tags=["posts"]),
)
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
@extend_schema(
summary="記事の公開処理",
description="下書き状態の記事を公開に切り替えます。作成者のみ可能。",
request=None,
responses={
200: inline_serializer(
name="PublishResponse",
fields={"published": serializers.BooleanField()},
),
403: OpenApiResponse(description="作成者ではない"),
404: OpenApiResponse(description="記事がない"),
},
tags=["posts"],
)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
post = self.get_object()
post.published = True
post.save()
return Response({"published": True})@extend_schema_view の使いどころ
#
ViewSet の自動メソッド (list、retrieve、create、…) は私たちが直接定義しないので、デコレータを付けられません。@extend_schema_view がクラスレベルでそれらのメソッドに一度にデコレータを付けてくれます。
request=None
#
publish アクションは本文を受け取りません。request=None で明示 — そうしないと spectacular が serializer_class (PostSerializer) を推論して間違った schema になります。
inline_serializer
#
レスポンスの形式が小さければ別途 Serializer クラスを作らずインラインで。大きい構造は別途 Serializer クラスのほうが再利用に向いています。
レスポンス例 — OpenApiExample
#
@extend_schema(
responses={
200: PostSerializer,
},
examples=[
OpenApiExample(
"公開された記事",
value={
"id": 1,
"title": "Django をはじめる",
"body": "...",
"published": True,
"created_at": "2026-05-01T10:00:00Z",
},
response_only=True,
),
OpenApiExample(
"下書き",
value={
"id": 2,
"title": "作成中",
"body": "...",
"published": False,
"created_at": "2026-05-02T11:00:00Z",
},
response_only=True,
),
],
)
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)Swagger UI で例をトグルで見られるので、クライアント開発者が素早く感覚をつかめます。
エラースキーマ — 一貫した形式 #
DRF のデフォルトのエラーレスポンスは形式がまちまちです (validation は dict、exception は {"detail": "..."})。一貫したエラー envelope を作って、それをすべての 4xx/5xx レスポンスに明示するのが良いです。
from rest_framework import serializers
class ErrorDetailSerializer(serializers.Serializer):
detail = serializers.CharField()
code = serializers.CharField(required=False)
field_errors = serializers.DictField(
child=serializers.ListField(child=serializers.CharField()),
required=False,
)@extend_schema(
responses={
201: PostSerializer,
400: ErrorDetailSerializer,
401: ErrorDetailSerializer,
403: ErrorDetailSerializer,
},
)
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)グローバルエラーハンドラと一緒に #
REST_FRAMEWORK = {
...,
"EXCEPTION_HANDLER": "blog.exceptions.custom_exception_handler",
}from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is None:
return None
data = {"detail": response.data.get("detail", str(exc))}
if isinstance(response.data, dict) and "detail" not in response.data:
data = {"detail": "Validation failed", "field_errors": response.data}
response.data = data
return responseレスポンスの形式が 常に同じ ならクライアントコードがシンプルになり、schema も正直になります。
認証ヘッダの明細 #
#2 で作った JWT 認証を Swagger UI の「Authorize」ボタンが認識するように — 自動でも拾われますが、明示的に押さえたければ。
SPECTACULAR_SETTINGS = {
...,
"SECURITY": [{"BearerAuth": []}],
"AUTHENTICATION_WHITELIST": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"APPEND_COMPONENTS": {
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
}
}
},
}Swagger UI で /api/auth/token/ からトークンを取得し、「Authorize」ボタンに Bearer <access> を入力すれば以後すべてのリクエストにヘッダが自動で付きます — Try it out で認証された呼び出しをすぐに試せます。
クエリパラメータの明細 — OpenApiParameter
#
@action やカスタム view でクエリパラメータを明示。
@extend_schema(
parameters=[
OpenApiParameter(
name="since",
type=str,
location=OpenApiParameter.QUERY,
description="ISO 8601 datetime — それ以降の記事だけ",
required=False,
examples=[OpenApiExample("最近 7 日", value="2026-05-01T00:00:00Z")],
),
],
responses={200: PostSerializer(many=True)},
)
@action(detail=False, methods=["get"])
def recent(self, request):
...DjangoFilterBackend、OrderingFilter、SearchFilter のパラメータは spectacular が自動で拾うので別途書く必要はありません。自動推論が拾えないものだけ補います。
Versioning — API バージョン #
REST_FRAMEWORK = {
...,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1", "v2"],
}urlpatterns = [
path("api/<version>/", include("blog.urls")),
]drf-spectacular がバージョンごとに別の schema を作ってくれます。クライアントが Accept-Version: v2 のようなヘッダで分岐できます。
クライアント SDK の自動生成 #
OpenAPI の大きな効用 — TypeScript / Swift / Kotlin SDK の自動生成。フロントが API 呼び出しコードを直接書かなくて済みます。
# TypeScript の型だけ
npx openapi-typescript http://localhost:8000/api/schema/ -o api.d.ts
# フル SDK (axios クライアント + 型)
npx @openapitools/openapi-generator-cli generate \
-i http://localhost:8000/api/schema/ \
-g typescript-axios \
-o ./clientbackend が spec を更新したら frontend がその SDK を再生成 — 型安全な contract first の流れ。
Schema 回帰テスト — breaking change の検知 #
#6 で深く扱いますが、パターンはシンプルです。CI で schema diff をチェックして意図しない breaking change を防ぎます。
uv run python manage.py spectacular --file schema.yml --validate
git diff --exit-code schema.yml # 変更されたら PR で意図したものか問うoasdiff のような専用ツールはより精緻な diff と breaking 分類をしてくれます。
FastAPI の自動 OpenAPI との比較 #
FastAPI #1 で見たように FastAPI は ビルトイン で自動ドキュメントが出てきます。DRF は別途のライブラリ (spectacular) + デコレータがさらに必要です。
| Django + DRF + spectacular | FastAPI | |
|---|---|---|
| 自動ドキュメント | spectacular のインストール / 設定 | ビルトイン (/docs) |
| 自動推論 | ViewSet/Serializer ベース | 関数シグネチャ / Pydantic ベース |
| 追加明細 | @extend_schema デコレータ | 関数シグネチャ + response_model |
| 明示度 | デコレータがさらに必要 | シグネチャ自体が明細 |
| 結果の品質 | 非常に良い | 非常に良い |
| 学習曲線 | spectacular のパターン学習 | ほぼなし |
すでに大きな Django プロジェクトがあるなら spectacular を載せるコストは小さく効用は大きいです。両方とも結局 OpenAPI 3.x に収束するのでクライアント SDK 生成 / 外部ツールは同じ。
よく出会う落とし穴 #
1) 自動推論されたスキーマがぎこちない #
fields = "__all__" で作った ModelSerializer が password_hash のようなフィールドを公開。明示的な fields が安全です (#1 でも同じ推奨)。
2) レスポンスが複数 #
PostSerializer(many=True) vs PostSerializer() のように、同じアクションがコンテキストによって異なるレスポンスを返すとき、responses={200: PostSerializer(many=True)} のように 明示 してください。自動推論が常に正しいとは限りません。
3) @action の serializer_class
#
@action で別の入力 / 出力を使うなら明示的に:
@extend_schema(request=PublishRequestSerializer, responses=PublishResponseSerializer)
@action(detail=True, methods=["post"], serializer_class=PublishRequestSerializer)
def publish(self, request, pk=None): ...4) schema が stale #
コードを変えたのに schema が昔の形式。manage.py spectacular --file ... --validate が import の順序やジェネリックエラーを拾ってくれます。CI に追加 してください — schema 生成が壊れたらビルド失敗で。
5) EXCEPTION_HANDLER と schema の不一致
#
カスタム例外ハンドラでレスポンスの形式を変えたのに schema には古い形式。例外ハンドラが作るレスポンスの形式を responses= に正確に明示。
まとめ #
今回つかんだもの:
- 自動ドキュメントが価値ある場面 — 契約、SDK、テスト、外部公開、オンボーディング
- drf-spectacular vs drf-yasg — OpenAPI 3.x 時代は spectacular
- インストール +
SPECTACULAR_SETTINGS+ URL ルート —/schema/、/docs/、/redoc/ manage.py spectacular --validateで CI ゲート- 自動推論で得るもの — Serializer / フィルタ / ページネーション / 権限 / @action
@extend_schema— summary / description / request / responses / tags@extend_schema_view— ViewSet の自動メソッドにデコinline_serializer、OpenApiExample、OpenApiResponse、OpenApiParameter- 一貫したエラースキーマ + グローバル
EXCEPTION_HANDLER - JWT 認証の明細、Swagger UI の Authorize 動作
- API versioning
- クライアント SDK の自動生成 (
openapi-typescript、openapi-generator) - Schema 回帰テストで breaking change の検知
- FastAPI のビルトイン自動ドキュメントとの比較
- 落とし穴 —
__all__、複数レスポンス、@actionserializer、stale schema、例外ハンドラ
FastAPI #1 のビルトイン /docs ほど自然ではないですが、spectacular で明示的なスキーマを作るやり方は結果的に より正確なドキュメント をくれます — 自動推論が拾えない複数レスポンス / エラースキーマ / 例まで全部入るからです。
次回 (#6 テストとデプロイ) ではシリーズの最後 — APITestCase / APIClient / pytest-django で自動テストを書き、Docker / gunicorn / nginx でプロダクションデプロイまで — 一カ所に整理します。