drf-yasgを試してみる

django-rest-swaggerを今まで使っていましたが、公式が非推奨[^1]ということで、代替手段として[drf-yasg](https://github.com/axnsan12/drf-yasg)を試してみる。


[^1]: https://github.com/marcgibbons/django-rest-swagger のREADMEを参照

# セットアップ

公式の[Quickstart](https://github.com/axnsan12/drf-yasg#quickstart)を参照すると、ひとまず使えるようになる。

# デフォルトの動き

[Custom schema generation — drf\-yasg 1\.0\.4 documentation](https://drf-yasg.readthedocs.io/en/1.0.4/custom_spec.html#default-behavior)を参照

- 各ViewSetのserializer_classに代入しているSerializerを元に、パラメータ・レスポンスを生成してくれる
- list(GET)の場合、Paginationクラスで指定しているフィールドと、filter_backendsに設定されているものがqueryパラメータとして表示してくれる
    - querysetをオーバーライドしてフィルタリングを実現している場合は、検索用パラメータは別個で指定が必要
- Serializierの中で更にSerializerがネストしている場合は、read_only、write_onlyが上手く効いてくれない
    - [inspecting serializers doesn't honor read\_only=True flag on nested serializers · Issue \#239 · axnsan12/drf\-yasg](https://github.com/axnsan12/drf-yasg/issues/239)
    - 標準機能ではないけど、[ここのSchema](https://github.com/axnsan12/drf-yasg/issues/70#issuecomment-485050813)を使うようにすると、ある程度解決できる。

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

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

```python
@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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168063/e535e753-0eab-e031-3db5-c58cfe8603f0.png)


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

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


```python
@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ステータスだけ返したいパターンがあると思います。

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

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168063/f55d4104-558b-ac3b-a88d-cdd48630a55e.png)

# 不必要なAPIを削除する

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

```python
@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で書いていきます。

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

    # Markdownも使える
    
    - 箇条
    - 書きも
    - できるよ
    """
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168063/1d396d21-d215-ba6f-ae05-7cf22c2f148c.png)

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

swagger_auto_schemaの`operation_description`を使用する

```python
@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](https://github.com/axnsan12/drf-yasg/issues/165)

なお、OpenAPI 3では対応されている模様。
[writeOnly field · Issue \#425 · OAI/OpenAPI\-Specification](https://github.com/OAI/OpenAPI-Specification/issues/425)

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

```python
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

```

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

    # スキーマ
    swagger_schema = ReadWriteAutoSchema
```

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

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

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

```python
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` は出てきて欲しくないが、出てきてしまう。
こればかりは仕方がないので、個別のスキーマクラスを定義する。

```python
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

```

```python
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_list` が`many=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

```python

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

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

```python

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

```

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

```

```python: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` で参照するようにします。


# 参考

- [Custom schema generation — drf\-yasg 1\.0\.4 documentation](https://drf-yasg.readthedocs.io/en/1.0.4/custom_spec.html#default-behavior)
- [Filtering \- Django REST framework](https://www.django-rest-framework.org/api-guide/filtering/)