Django DRF #5 OpenAPI ドキュメント — drf-spectacular

読了 9分

#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-yasgdrf-spectacular
OpenAPI バージョン2.0 (Swagger)3.x
アクティブ度メンテナンスモード活発
型ヒント推論普通強い
推奨 (現在)新規はこちら

OpenAPI 3.x が事実上の標準になった今、新規プロジェクトは drf-spectacular が最初の選択。この記事も spectacular ベースです。

インストール #

パッケージ
uv add drf-spectacular
settings.py
INSTALLED_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 ルート #

mysite/urls.py
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 ファイルを出力したいなら。

schema ファイル生成
uv run python manage.py spectacular --file schema.yml --validate

--validate が OpenAPI 3.x 仕様に違反しないかをチェック。CI でこれがパスするのが最初のゲート。

自動推論 — 何が無料で得られるか #

drf-spectacular は ViewSet/Serializer だけで 既に豊かなスキーマ を作ってくれます。

出処スキーマに変換
serializer_classrequest / response body
queryset.modelresponse のオブジェクト型
filter_backendsfilterset_classクエリパラメータ (フィルタ)
ordering_fieldssearch_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 — 精密な明細 #

自動推論が拾えない部分 — レスポンス例、複数レスポンス、追加パラメータ、エラースキーマ — はデコレータで埋めます。

blog/views.py
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 の自動メソッド (listretrievecreate、…) は私たちが直接定義しないので、デコレータを付けられません。@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 レスポンスに明示するのが良いです。

blog/serializers.py — エラー
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,
    )
ViewSet で
@extend_schema(
    responses={
        201: PostSerializer,
        400: ErrorDetailSerializer,
        401: ErrorDetailSerializer,
        403: ErrorDetailSerializer,
    },
)
def create(self, request, *args, **kwargs):
    return super().create(request, *args, **kwargs)

グローバルエラーハンドラと一緒に #

settings.py
REST_FRAMEWORK = {
    ...,
    "EXCEPTION_HANDLER": "blog.exceptions.custom_exception_handler",
}
blog/exceptions.py
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」ボタンが認識するように — 自動でも拾われますが、明示的に押さえたければ。

settings.py
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):
    ...

DjangoFilterBackendOrderingFilterSearchFilter のパラメータは spectacular が自動で拾うので別途書く必要はありません。自動推論が拾えないものだけ補います。

Versioning — API バージョン #

settings.py
REST_FRAMEWORK = {
    ...,
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
    "DEFAULT_VERSION": "v1",
    "ALLOWED_VERSIONS": ["v1", "v2"],
}
urls.py
urlpatterns = [
    path("api/<version>/", include("blog.urls")),
]

drf-spectacular がバージョンごとに別の schema を作ってくれます。クライアントが Accept-Version: v2 のようなヘッダで分岐できます。

クライアント SDK の自動生成 #

OpenAPI の大きな効用 — TypeScript / Swift / Kotlin SDK の自動生成。フロントが API 呼び出しコードを直接書かなくて済みます。

openapi-generator または openapi-typescript
# 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 ./client

backend が 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 + spectacularFastAPI
自動ドキュメント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) @actionserializer_class #

@action で別の入力 / 出力を使うなら明示的に:

@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_serializerOpenApiExampleOpenApiResponseOpenApiParameter
  • 一貫したエラースキーマ + グローバル EXCEPTION_HANDLER
  • JWT 認証の明細、Swagger UI の Authorize 動作
  • API versioning
  • クライアント SDK の自動生成 (openapi-typescriptopenapi-generator)
  • Schema 回帰テストで breaking change の検知
  • FastAPI のビルトイン自動ドキュメントとの比較
  • 落とし穴 — __all__、複数レスポンス、@action serializer、stale schema、例外ハンドラ

FastAPI #1 のビルトイン /docs ほど自然ではないですが、spectacular で明示的なスキーマを作るやり方は結果的に より正確なドキュメント をくれます — 自動推論が拾えない複数レスポンス / エラースキーマ / 例まで全部入るからです。

次回 (#6 テストとデプロイ) ではシリーズの最後 — APITestCase / APIClient / pytest-django で自動テストを書き、Docker / gunicorn / nginx でプロダクションデプロイまで — 一カ所に整理します。

X