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が上手く効いてくれない
- inspecting serializers doesn't honor read_only=True flag on nested serializers · Issue #239 · axnsan12/drf-yasg
- 標準機能ではないけど、ここのSchemaを使うようにすると、ある程度解決できる。
クエリパラメータを設定する
検索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):
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):
不必要な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も使える
- 箇条
- 書きも
- できるよ
"""
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
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_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
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',
}
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
で参照するようにします。