LoginSignup
10
7

More than 3 years have passed since last update.

drf-yasgを試してみる

Last updated at Posted at 2020-08-11

django-rest-swaggerを今まで使っていましたが、公式が非推奨1ということで、代替手段としてdrf-yasgを試してみる。

セットアップ

公式のQuickstartを参照すると、ひとまず使えるようになる。

デフォルトの動き

Custom schema generation — drf-yasg 1.0.4 documentationを参照

  • 各ViewSetのserializer_classに代入しているSerializerを元に、パラメータ・レスポンスを生成してくれる
  • list(GET)の場合、Paginationクラスで指定しているフィールドと、filter_backendsに設定されているものがqueryパラメータとして表示してくれる
    • querysetをオーバーライドしてフィルタリングを実現している場合は、検索用パラメータは別個で指定が必要
  • Serializierの中で更にSerializerがネストしている場合は、read_only、write_onlyが上手く効いてくれない

クエリパラメータを設定する

検索APIを、querysetをオーバーライドで実現している場合は、個別でswagger_auto_schemaデコレータを、viewsetの関数につけてあげる。

@swagger_auto_schema(
        manual_parameters=[
            openapi.Parameter(
                "parameter1",
                openapi.IN_QUERY,
                description="パラメータ1",
                type=openapi.TYPE_ARRAY,
                items=openapi.Items(type=openapi.TYPE_STRING),
            ),
            openapi.Parameter(
                "parameter2",
                openapi.IN_QUERY,
                type=openapi.TYPE_STRING,
                description="パラメータ2_enum",
                enum=['type1', 'type2'],
            ),
        ],
def list(self, request, *args, **kwargs):

image.png

viewsetの関数をオーバーライドしていない場合

viewsetの関数をオーバーライドしていないときは、djangoのmethod_decoratorを使うことで、上のと同じ意味になる

@method_decorator(
    name="list",
    decorator=swagger_auto_schema(
        manual_parameters=[
            openapi.Parameter(
                "parameter1",
                openapi.IN_QUERY,
                description="パラメータ1",
                type=openapi.TYPE_ARRAY,
                items=openapi.Items(type=openapi.TYPE_STRING),
            ),
            openapi.Parameter(
                "parameter2",
                openapi.IN_QUERY,
                type=openapi.TYPE_STRING,
                description="パラメータ2_enum",
                enum=['type1', 'type2'],
            ),
        ],
    )
)
class HogeViewSet(viewsets.ModelViewSet):

レスポンスを指定する

デフォルトだと、GET、POST、PUTなどのレスポンスは、シリアライザーのモデルが返ってくるようになります。

ですが、POSTは成功したらHTTPステータスだけ返したいパターンがあると思います。

@swagger_auto_schema(responses={201: 'ok', 400: 'バリデーションエラー'})
def create(self, request, *args, **kwargs):

image.png

不必要なAPIを削除する

デフォルト表示だと、REST APIのすべてのメソッド(GET、POST、PUT、PATCH、DELETE)が表示されるようになっています。
その中で要らないものを省く時には、 swagger_auto_schema(auto_schema=None) を指定するとswagger上から削除できます。

@method_decorator(
    name="destroy",
    decorator=swagger_auto_schema(auto_schema=None)
)
@method_decorator(
    name="partial_update",
    decorator=swagger_auto_schema(auto_schema=None)
)
class HogeViewSet(viewsets.ModelViewSet):

APIの説明を追加する

swaggerにAPIの説明を追加したい場合は、viewsetの関数にdocstringで書いていきます。

def create(self, request, *args, **kwargs):
    """
    登録APIです。

    # Markdownも使える

    - 箇条
    - 書きも
    - できるよ
    """

image.png

method_decoratorでデコレートしている場合

swagger_auto_schemaのoperation_descriptionを使用する

@method_decorator(
    name="retrieve",
    decorator=swagger_auto_schema(
        operation_description=
        """
        詳細取得APIです
        """
        )
)

ネストしたwrite_only、read_onlyを利かせるようにする

drf-yasgが対応しているOpenAPI 2の仕様が、そもそもwrite_onlyがサポートされていないらしい。
A field having `write_only=True` displays in response schema. · Issue #165 · axnsan12/drf-yasg

なお、OpenAPI 3では対応されている模様。
writeOnly field · Issue #425 · OAI/OpenAPI-Specification

そのため、以下のIssueで提案されているSchemaを使用する。
https://github.com/axnsan12/drf-yasg/issues/70#issuecomment-485050813

from collections import OrderedDict

from drf_yasg.inspectors import SwaggerAutoSchema
# OpenAPI 2 だと、write_onlyが正しく反映されない問題があるため、このスキーマを間に使う
# 参照:https://github.com/axnsan12/drf-yasg/issues/70#issuecomment-485050813
from drf_yasg.utils import no_body


class BlankMeta:
    pass

class ReadOnly:
    def get_fields(self):
        new_fields = OrderedDict()
        for fieldName, field in super().get_fields().items():
            if not field.write_only:
                new_fields[fieldName] = field

        return new_fields

class WriteOnly:
    def get_fields(self):
        new_fields = OrderedDict()
        for fieldName, field in super().get_fields().items():
            if not field.read_only:
                new_fields[fieldName] = field
        return new_fields

class ReadWriteAutoSchema(SwaggerAutoSchema):
    def get_view_serializer(self):
        return self._convert_serializer(WriteOnly)

    def get_default_response_serializer(self):
        body_override = self._get_request_body_override()
        if body_override and body_override is not no_body:
            return body_override

        return self._convert_serializer(ReadOnly)


    def _convert_serializer(self, new_class):
        serializer = super().get_view_serializer()
        if not serializer:
            return serializer

        class CustomSerializer(new_class, serializer.__class__):
            class Meta(getattr(serializer.__class__, 'Meta', BlankMeta)):
                ref_name = new_class.__name__ + serializer.__class__.__name__

        new_serializer = CustomSerializer(data=serializer.data)
        return new_serializer

viewsetでの使い方
class HogeViewSet(viewsets.ModelViewSet):
    ・・・

    # スキーマ
    swagger_schema = ReadWriteAutoSchema

ネストしたシリアライザーの更にネストでの、write_only、read_onlyを利かせるようにする

上のスキーマクラスを使用するとある程度解決するけど、更にネストしているシリアライザーがいた場合には、無理となる。

たとえばcreate(POST)の場合で、以下の構成のシリアライザーがあった場合、

class NestedSerializer:
    nested_of_nested_field = NestedOfNestedSerializer(read_only=True)

    class Meta:
        #: モデル
        model = NestedModel

        #: フィールド
        fields = (・・・, 'nested_of_nested_field',)

class TopSerializer:
    # ネストシリアライザーを指定
    nested_list = NestedSerializer(
        write_only=True,
        many=True,
    )

このとき、POSTのrequest_bodyには、nested_of_nested_field は出てきて欲しくないが、出てきてしまう。
こればかりは仕方がないので、個別のスキーマクラスを定義する。

class NestedCreateWriteOnly:
    def get_fields(self):
        new_fields = OrderedDict()
        for fieldName, field in super().get_fields().items():
            if fieldName == 'nested_list':
                # nested_list が消えた状態でキャッシュされている可能性があるので、その回避策
                if 'nested_list' in field.child._declared_fields:
                    field.child._declared_fields.pop('nested_list')

                fields_tuple = list(field.child.Meta.fields)
                update_fields = [f for f in fields_tuple if f != 'nested_list']
                field.child.Meta.fields = tuple(update_fields)
                new_fields[fieldName] = field
                continue

            if not field.read_only:
                new_fields[fieldName] = field
        return new_fields

class NestedSchema(ReadWriteAutoSchema):
    def get_view_serializer(self):
        if self.view.action == 'create':
            return super()._convert_serializer(NestedCreateWriteOnly)
        else:
            return super().get_view_serializer()

これをswagger_schemaに設定すれば、create(POST)のみ、NestedCreateWriteOnly のget_fieldsで指定されたルールで、除去をしてくれる。

nested_listmany=True で指定されているため、中身はListSerializerとなっているので、child から定義されているフィールドを取得している。

_declared_fields と、 Meta情報、ともに削除する必要がある。

LoginしたUserに応じて、表示するAPIエンドポイントを変える

LoginしたUserが叩くことができるAPIのみを表示したいということがあると思います。

その時には、get_schema_view の public=Falseと指定をすれば良いそうです。(defaultはFalseなので、消してしまってもいいのかも)
https://drf-yasg.readthedocs.io/en/stable/drf_yasg.html?highlight=get_schema_view#drf_yasg.views.get_schema_view


schema_view_yasg = get_schema_view(
    api_info,
    public=False,
    permission_classes=(permissions.AllowAny,),
    authentication_classes=(authentication.SessionAuthentication,),
)

これをすれば、不必要なAPIを削除するで、やっていたauto_schema=Noneを指定せずとも、 get_permissions でDenyされているものは、swagger上では表示されなくなります。


def get_permissions(self):
    """
    パーミッション設定
    """
    if self.action in ['create', 'retrieve', 'update', 'partial_update', 'destroy']:
        return [DenyAll()]

    return [permission() for permission in self.permission_classes]

今あるコードから、swagger.yamlファイルを生成したい

localhost上で見るのではなくて、ファイルとして吐き出しておきたいということもあると思います。

manage.pyのコマンドに追加された generate_swagger を使用して、ファイルを生成することができます。
https://drf-yasg.readthedocs.io/en/stable/rendering.html#management-command

python manage.py generate_swagger -o -f yaml swagger.yaml

この時に、何も設定していないと以下のようなエラーが出てきますので、settings.pyとurls.pyを少し修正する必要があります。

  File "/usr/local/lib/python3.7/site-packages/drf_yasg/management/commands/generate_swagger.py", line 120, in handle
    'settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an '
django.core.exceptions.ImproperlyConfigured: settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an import string pointing to an openapi.Info object

SWAGGER_SETTINGS = {
    ・・・
    'DEFAULT_INFO': 'xxx.api_info',
}

urls.py
api_info = openapi.Info(
    title="Snippets API",
    default_version='v1',
    description="test",
    terms_of_service="https://www.google.com/policies/terms/",
    contact=openapi.Contact(email="contact@snippets.local"),
    license=openapi.License(name="BSD License"),
)
schema_view_yasg = get_schema_view(
    api_info,
    public=False,
    permission_classes=(permissions.AllowAny,),
    authentication_classes=(authentication.SessionAuthentication,),
)

openapi.infoの値を変数に外出ししておき、それをDEFAULT_INFO で参照するようにします。

参考

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7