Masayan tech blog.

  1. ブログ記事一覧>
  2. Django REST APIのドキュメントを自動生成する

Django REST APIのドキュメントを自動生成する

公開日

この記事を読むとできるようになること

  • drf-spectacularの概要と特徴が理解できる
  • drf-spectacularを使用して、Djangoで作成したAPIのドキュメントを作成するための方法が理解できる

drf-spectacularとは

  • Django REST API ドキュメントを実装から自動生成するためのライブラリ
  • OpenAPI3.0に対応
  • yaml, redoc,swagger形式に対応
  • Djangoのシリアライザーからスキーマを自動検知できるので、基本的には細かな調整のみ手動で定義する

drf_yasgとの違い

  • drf-yasgはdrf-spectacular同様に、Django REST API ドキュメントを自動生成するためのライブラリ
  • OpenAPI 2.0にのみ対応。3.0には対応していない
  • drf-yasgでは、RedocとSwagger UIはパッケージ化されていたが、drf-spectacularではパッケージではなく、ハイパーリンクCDN経由で利用する
  • drf-spectacularでuiをカスタマイズしたい場合やオフラインで利用したい場合は、別パッケージのdrf-spectacular-sidecarを使用する必要がある
  • drf_yasgとdrf-spectacularでは、項目名が微妙に変更されている(Ex. swagger_auto_schema → @extend_schema

要件

Python 3.7以上。そのほかは以下のとおり
https://drf-spectacular.readthedocs.io/en/latest/readme.html#requirements

セットアップ

インストール

脆弱性チェック(参考までに)

pip install drf-spectacular

インストールされているアプリに drf-spectacular を追加し、REST_FRAMEWORKにDEFAULT_SCHEMA_CLASSを追加。

# settings.py

INSTALLED_APPS = [
    #・・・
    'drf_spectacular',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

エンドポイント定義

urls.pyに以下を追加する。viewはすでにライブラリ側で定義されているのでそれをそのまま使用する

from django.urls import path
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularRedocView,
    SpectacularSwaggerView,
)



urlpatterns = [
    path(
        "schema/",
        SpectacularAPIView.as_view(),
        name="schema",
    ),
    path(
        "docs/",
        SpectacularSwaggerView.as_view(
            url_name="schema"),
        name="swagger-ui",
    ),
    path(
        "redoc/",
        SpectacularRedocView.as_view(
            url_name="schema"),
        name="redoc",
    ),
]

schema/

ライブラリによって自動的に生成されたドキュメントを含むyamlファイルをダウンロードできる

docs/

ブラウザでSwagger UIを開くことができる

redoc/

ドキュメントのRedocモードが表示される

基本的な仕組み

  • viewにシリアライザを定義していれば、それだけでスキーマが検出され、ドキュメントが自動生成される
  • views.APIViewを継承したviewを使用している場合、シリアライザが不要な場合や、シリアライザだけではうまく表現できない場合、単純なレスポンスの場合、わざわざ明示的にシリアライザークラスを書く必要がない場合はextend_schemaデコレーターを使って手動でオーバーライドすることが可能
    • ListAPIView や RetrieveAPIView などの汎用 APIView 系ビューや ModelViewSet 系ビューなど(serializer_classを指定するもの)は入出力のパラメータが自動判定されるので、自動的にスキーマ定義される
  • djangoのシリアライザを使用する際の注意点として、カスタムのSerializerField(SerializerMethodField)は検出されないため、@extend_schema_fieldを付与する必要がある
# https://drf-spectacular.readthedocs.io/en/latest/customization.htmlより引用
class ErrorDetailSerializer(serializers.Serializer):
    field_custom = serializers.SerializerMethodField()

    @extend_schema_field(OpenApiTypes.DATETIME)
    def get_field_custom(self, object):
        return '2020-03-06 20:54:00.104248'

スキーマを手動で定義する

extend_schemaを使用してスキーマを定義する方法

このデコレータをビューのメソッドに指定することで、クエリパラメータ、リクエスト、レスポンスなどのスキーマを定義できる

extend_schema_viewを使用してスキーマを定義する方法

このデコレータをビュー全体に指定することでスキーマを定義することも可能。本記事ではこちらを中心に紹介する

例として、ArticleCommentViewという記事のコメントに関するビューがあると仮定する

class ArticleCommentView(ModelViewSet):

    def list(self, request, *args, **kwargs):
        pass

    def create(self, request, *args, **kwargs):
        pass

このようなviewに以下のようにextend_schema_viewデコレーターをメソッドごとに付与する

@extend_schema_view(
    # /articles/{article_id}/commentsにgetリクエストを送るときのスキーマ
    list=extend_schema(),
    # /articles/{article_id}/commentsにpostリクエストを送るときのスキーマ
    create=extend_schema(),
)
class ArticleCommentView(ModelViewSet):

    def list(self, request, *args, **kwargs):
        pass

    def create(self, request, *args, **kwargs):
        pass

extend_schemaに定義できる主な項目

  • OpenApiParameterでパスパラメーターやクエリパラメーターを指定できる
  • OpenApiRequestでリクエストボディを指定できる
  • OpenApiResponseでレスポンスボディを指定できる
  • OpenApiExampleでパラメーターやレスポンスの例を指定できる
  • operation_idで特定のAPIエンドポイントを一意に識別するための文字列を指定できる(デフォルトだと、djangoが自動的に英語で命名を付与する)

operation_idを定義した場合以下の箇所に適用される


以下で、extend_schemaを使用したArticleCommentViewのスキーマ定義例を示す

一覧取得

全体像

@extend_schema_view(
    # /articles/{article_id}/commentsにgetリクエストを送るときのスキーマ
    list=extend_schema(
        operation_id="記事コメント一覧取得",
        # リクエストパラメータ
        parameters=[
            # 必須のパスパラメータ
            OpenApiParameter(
                name="article_id",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.PATH,
                description="記事ID",
                required=True,
                examples=[
                    OpenApiExample(
                        name="ArticleIdPathParamExampleSchema",
                        value=1,
                        summary="記事IDです",
                    )
                ],
            ),
            # オプションのクエリパラメータ
            OpenApiParameter(
                name="sort",
                type=OpenApiTypes.STR,
                location=OpenApiParameter.QUERY,
                description="ソート順",
                required=False,
                examples=[
                    OpenApiExample(
                        name="ArticleSortQueryParamExampleSchema",
                        value="asc",
                        summary="ソート順です",
                    )
                ],
            ),
        ],
        responses={
            200: OpenApiResponse(
                response=inline_serializer(
                    name="ArticleCommentViewListResponseBodySerializer",
                    fields={
                        "article_id": serializers.IntegerField(
                            help_text="記事ID",  # RESPONSE SCHEMAの説明
                        ),
                        "comments": inline_serializer(
                            name="ArticleCommentViewListSerializer",
                            fields={
                                "id": serializers.IntegerField(
                                    help_text="コメントID",  # RESPONSE SCHEMAの説明
                                ),
                                "comment": serializers.CharField(
                                    help_text="コメント内容",  # RESPONSE SCHEMAの説明
                                ),
                                "created_at": serializers.DateTimeField(
                                    help_text="コメント作成日時",  # RESPONSE SCHEMAの説明
                                ),
                                "updated_at": serializers.DateTimeField(
                                    help_text="コメント更新日時",  # RESPONSE SCHEMAの説明
                                ),
                            },
                            many=True,
                            help_text="この記事のコメント一覧",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="記事コメント一覧取得",
                examples=[
                    OpenApiExample(
                        name="レスポンスボディの例",
                        value={
                            "article_id": 1,
                            "comments": [
                                {
                                    "id": 1,
                                    "comment": "コメント内容",
                                    "created_at": "2021-01-01T00:00:00",
                                    "updated_at": "2021-01-01T00:00:00",
                                }
                            ],
                        },
                        description="レスポンスボディの説明",  # 右側のResponse samplsの例の説明
                    )
                ],
            )
        },
    ),

リクエストパラメーター

  • OpenApiParameterで定義
  • typeでデータ型を指定
  • パスパラメーターを定義したい場合はlocationをPATHで指定、クエリパラメーターはQUERYで指定
  • 必須のパラメーターはrequiredをTrueに、任意のパラメーターはFalseにする
  • examplesにリクエストパラメーターの具体例を複数指定可能
  • リクエストパラメーターに限らず、具体例を定義する際はOpenApiExampleを使用する
  • descriptionにはマークダウンを使用可能
@extend_schema_view(
    # /articles/{article_id}/commentsにgetリクエストを送るときのスキーマ
    list=extend_schema(
        operation_id="記事コメント一覧取得",
        # リクエストパラメータ
        parameters=[
            # 必須のパスパラメータ
            OpenApiParameter(
                name="article_id",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.PATH,
                description="記事ID",
                required=True,
                examples=[
                    OpenApiExample(
                        name="ArticleIdPathParamExampleSchema",
                        value=1,
                        summary="記事IDです",
                    )
                ],
            ),
            # オプションのクエリパラメータ
            OpenApiParameter(
                name="sort",
                type=OpenApiTypes.STR,
                location=OpenApiParameter.QUERY,
                description="ソート順",
                required=False,
                examples=[
                    OpenApiExample(
                        name="ArticleSortQueryParamExampleSchema",
                        value="asc",
                        summary="ソート順です",
                    )
                ],
            ),
        ],
        responses={
            
        },
    ),

レスポンスボディ

  • OpenApiResponseで定義。ステータスコード: OpenApiResponseの形で、ステータスコードごとに定義可能
  • 実際のレスポンスボディの定義はinline_serializerで行う(djangoのシリアライザを指定することも可)
  • inline_serializerにはnameでシリアライザを一意に定義し、fieldsにはレスポンスオブジェクトのフィールドを定義する
    • inline_serializerのnameが重複すると、正確にスキーマが反映されないので注意
  • 以下の例のように、レスポンスのあるフィールドが配列の場合は、さらに配列の要素を定義できるようにinline_serializerをネストさせる
    • ネストされたフィールドが配列として定義したい場合はシリアライザでmany=Trueを指定する
  • 各フィールドには、help_textが使用でき、これらはブラウザで表示した際にこのフィールドの説明として表示される
  • examplesにはOpenApiExampleを使用して、レスポンスボディの具体例を定義できる
@extend_schema_view(
    # /articles/{article_id}/commentsにgetリクエストを送るときのスキーマ
    list=extend_schema(
        operation_id="記事コメント一覧取得",
        parameters=[],
        responses={
            200: OpenApiResponse(
                response=inline_serializer(
                    name="ArticleCommentViewListResponseBodySerializer",
                    fields={
                        "article_id": serializers.IntegerField(
                            help_text="記事ID",  # RESPONSE SCHEMAの説明
                        ),
                        "comments": inline_serializer(
                            name="ArticleCommentViewListSerializer",
                            fields={
                                "id": serializers.IntegerField(
                                    help_text="コメントID",  # RESPONSE SCHEMAの説明
                                ),
                                "comment": serializers.CharField(
                                    help_text="コメント内容",  # RESPONSE SCHEMAの説明
                                ),
                                "created_at": serializers.DateTimeField(
                                    help_text="コメント作成日時",  # RESPONSE SCHEMAの説明
                                ),
                                "updated_at": serializers.DateTimeField(
                                    help_text="コメント更新日時",  # RESPONSE SCHEMAの説明
                                ),
                            },
                            many=True,
                            help_text="この記事のコメント一覧",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="記事コメント一覧取得",
                examples=[
                    OpenApiExample(
                        name="レスポンスボディの例",
                        value={
                            "article_id": 1,
                            "comments": [
                                {
                                    "id": 1,
                                    "comment": "コメント内容",
                                    "created_at": "2021-01-01T00:00:00",
                                    "updated_at": "2021-01-01T00:00:00",
                                }
                            ],
                        },
                        description="レスポンスボディの説明",  # 右側のResponse samplsの例の説明
                    )
                ],
            )
        },
    ),
)

新規追加

全体像

@extend_schema_view(
    # /articles/{article_id}/commentsにpostリクエストを送るときのスキーマ
    create=extend_schema(
        operation_id="記事コメント新規追加",
        parameters=[
            OpenApiParameter(
                name="article_id",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.PATH,
                description="記事ID",
                required=True,
                examples=[
                    OpenApiExample(
                        name="ArticleIdPathParamExampleSchema",
                        value=1,
                        summary="記事IDです",
                    )
                ],
            )
        ],
        # リクエストボディ
        request=OpenApiRequest(
            # リクエストボディ
            request=inline_serializer(
                name="ArticleCommentViewListRequestBodySerializer",
                fields={
                    "comment": serializers.CharField(
                        required=True,
                        help_text="コメント内容",  # REQUEST BODY SCHEMAの説明
                    ),
                },
            ),
            # リクエストボディの例
            examples=[
                OpenApiExample(
                    name="リクエストボディの例",
                    value={"comment": "コメント内容"},
                    description="リクエストボディの例",  # 右側のpayloadの例の説明
                )
            ],
        ),
        # レスポンスボディ
        responses={
            202: OpenApiResponse(
                response=inline_serializer(
                    name="ArticleCommentViewListResponseBodySerializer",
                    fields={
                        "article_id": serializers.IntegerField(
                            help_text="記事ID",  # RESPONSE SCHEMAの説明
                        ),
                        "comments": inline_serializer(
                            name="ArticleCommentViewListSerializer",
                            fields={
                                "id": serializers.IntegerField(
                                    help_text="コメントID",  # RESPONSE SCHEMAの説明
                                ),
                                "comment": serializers.CharField(
                                    help_text="コメント内容",  # RESPONSE SCHEMAの説明
                                ),
                                "created_at": serializers.DateTimeField(
                                    help_text="コメント作成日時",  # RESPONSE SCHEMAの説明
                                ),
                                "updated_at": serializers.DateTimeField(
                                    help_text="コメント更新日時",  # RESPONSE SCHEMAの説明
                                ),
                            },
                            many=True,
                            help_text="この記事のコメント一覧",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="記事コメント新規追加",
                examples=[
                    OpenApiExample(
                        name="レスポンスボディの例",
                        value={
                            "article_id": 1,
                            "comments": [
                                {
                                    "id": 1,
                                    "comment": "コメント内容",
                                    "created_at": "2021-01-01T00:00:00",
                                    "updated_at": "2021-01-01T00:00:00",
                                }
                            ],
                        },
                        description="レスポンスボディの説明",  # 右側のResponse samplsの例の説明
                    )
                ],
            ),
            400: OpenApiResponse(
                response=inline_serializer(
                    name="BadRequestSerializer",
                    fields={
                        "detail": serializers.CharField(
                            help_text="エラーメッセージ",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="Bad Request",
            ),
        },
    ),
)

リクエストパラメーター

基本的には一覧取得の時の定義と同じなので割愛

@extend_schema_view(
    # /articles/{article_id}/commentsにpostリクエストを送るときのスキーマ
    create=extend_schema(
        operation_id="記事コメント新規追加",
        parameters=[
            OpenApiParameter(
                name="article_id",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.PATH,
                description="記事ID",
                required=True,
                examples=[
                    OpenApiExample(
                        name="ArticleIdPathParamExampleSchema",
                        value=1,
                        summary="記事IDです",
                    )
                ],
            )
        ],
    ),
)

リクエストボディ

  • OpenApiRequestで定義
  • OpenApiRequestのrequestには、inline_serializerを定義する。使い方はレスポンスボディの定義の時と基本的には同じ
  • リクエストボディも他の項目と同様、OpenApiExampleを使用して具体例を定義可能
@extend_schema_view(
    # /articles/{article_id}/commentsにpostリクエストを送るときのスキーマ
    create=extend_schema(
        operation_id="記事コメント新規追加",
        parameters=[],
        # リクエストボディ
        request=OpenApiRequest(
            # リクエストボディ
            request=inline_serializer(
                name="ArticleCommentViewListRequestBodySerializer",
                fields={
                    "comment": serializers.CharField(
                        required=True,
                        help_text="コメント内容",  # REQUEST BODY SCHEMAの説明
                    ),
                },
            ),
            # リクエストボディの例
            examples=[
                OpenApiExample(
                    name="リクエストボディの例",
                    value={"comment": "コメント内容"},
                    description="リクエストボディの例",  # 右側のpayloadの例の説明
                )
            ],
        ),
        responses={},
    ),
)

レスポンスボディ

  • 400: OpenApiResponseとして正常系の他、複数のレスポンスのパターンを定義できる
@extend_schema_view(
    # /articles/{article_id}/commentsにpostリクエストを送るときのスキーマ
    create=extend_schema(
        operation_id="記事コメント新規追加",
        parameters=[ ],
        request=,
        responses={
            202: OpenApiResponse(
                response=inline_serializer(
                    name="ArticleCommentViewListResponseBodySerializer",
                    fields={
                        "article_id": serializers.IntegerField(
                            help_text="記事ID",  # RESPONSE SCHEMAの説明
                        ),
                        "comments": inline_serializer(
                            name="ArticleCommentViewListSerializer",
                            fields={
                                "id": serializers.IntegerField(
                                    help_text="コメントID",  # RESPONSE SCHEMAの説明
                                ),
                                "comment": serializers.CharField(
                                    help_text="コメント内容",  # RESPONSE SCHEMAの説明
                                ),
                                "created_at": serializers.DateTimeField(
                                    help_text="コメント作成日時",  # RESPONSE SCHEMAの説明
                                ),
                                "updated_at": serializers.DateTimeField(
                                    help_text="コメント更新日時",  # RESPONSE SCHEMAの説明
                                ),
                            },
                            many=True,
                            help_text="この記事のコメント一覧",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="記事コメント新規追加",
                examples=[
                    OpenApiExample(
                        name="レスポンスボディの例",
                        value={
                            "article_id": 1,
                            "comments": [
                                {
                                    "id": 1,
                                    "comment": "コメント内容",
                                    "created_at": "2021-01-01T00:00:00",
                                    "updated_at": "2021-01-01T00:00:00",
                                }
                            ],
                        },
                        description="レスポンスボディの説明",  # 右側のResponse samplsの例の説明
                    )
                ],
            ),
            400: OpenApiResponse(
                response=inline_serializer(
                    name="BadRequestSerializer",
                    fields={
                        "detail": serializers.CharField(
                            help_text="エラーメッセージ",  # RESPONSE SCHEMAの説明
                        ),
                    },
                ),
                description="Bad Request",
            ),
        },
    ),
)

リダイレクトレスポンス

レスポンスボディがないので単に以下のようになる

extend_schema(
  responses={
    301: OpenApiResponse(
      description="Redirect to another URL",
      examples={
          "application/json": {
          "detail": "Found",
          "url": "https://example.com"
        }
      }
    ),
  }
)

OpenApiExample

  • OpenApiExampleはextend_schemaのフィールドに指定するか、各OpenApiParameterやOpenApiRequest、OpenApiResponseに対して個別に定義するかの2つの方法がある
  • 前者の場合、どの項目(リクエストボディ or レスポンスボディ、レスポンスボディが異常系と正常系で複数ある場合どのステータスコードに紐づくものか等)に対する例なのかを明示的に指定する必要がある
  • 基本的には各OpenApiParameterやOpenApiRequest、OpenApiResponseに明示的に指定するほうが個人的にはわかりやすくて、以下のようなパラメータを毎回指定する必要もないのでこちらの方がおすすめ

request_onlyとresponse_only

request_onlyをFalse、response_onlyをTrueとすると、exampleをレスポンスにのみ適用できる。

status_codes

異常系、正常系で複数のレスポンス定義がある場合、どのステータスコードに対するレスポンス例なのかをこのフィールドで指定する

@extend_schema_view(
    list=extend_schema(
        # extend_schemaのフィールドに指定している。この場合、どの項目(リクエスト、レスポンス、ステータスコード)に対する例なのかを明示的に指定する必要がある
        examples=[
            OpenApiExample(
                name="ArticleCommentViewListExample",
                value={
                    "article_id": 1,
                    "comments": [
                        {
                            "id": 1,
                            "comment": "コメント内容",
                            "created_at": "2021-01-01T00:00:00",
                            "updated_at": "2021-01-01T00:00:00",
                        }
                    ],
                },
                description="記事コメント一覧取得の例",
                status_codes=[200],
                request_only=False,
                response_only=True,
            )
        ],
  ・・・

エンドポイントにコメントや説明を追加する方法

さらに詳しく説明を追加したい場合は、対応するメソッドにdocstringを定義することで可能。マークダウン形式で書けるので、かなり使いやすい

class ArticleCommentView(ModelViewSet):

    def list(self, request, *args, **kwargs):
        """
        docstringでさらに説明を追加できます
        - a
        - b
        - c
        """
        pass

Swagger UIでの検証

認証が必要なエンドポイントに対して検証のリクエストを送信する際は、事前に認証する必要がある。認証が必要なエンドポイントには鍵のマークが表示されている

画面右上のAuthorizeボタンを押すと、モーダルで利用可能な認証方法が表示されるので、いずれかの方式でAuthorizeボタンを押して認証する

これにより、認証が必要なエンドポイントに対してのリクエストの検証を行うことができる

まとめ

いかがでしたでしょうか。本記事では、drf-spectacularを使用して、Django REST APIのドキュメントを自動生成する方法について紹介しました。効率的なAPI開発を行う上ではAPIのドキュメント定義は欠かせないものになりますので、ぜひ参考にしてみてください。