drf-yasg는 django-rest-framework으로 정의한 API를 문서화할 수 있는 패키지입니다.

drf-yasg의 Repository에서는 다음과 같이 소개하고 있습니다.

drf-yasg - Yet another Swagger generator

Generate real Swagger/OpenAPI 2.0 specifications from a Django Rest Framework API.

drf-yasg를 단순히 적용만 해도 정의한 모델, API 목록을 볼 수 있는 문서를 생성할 수 있으며, 필요에 따라 개발자가 내용을 추가하거나 수정할 수 있습니다.

다만 처음 적용할 때라면 (제 기준에서) 적용후기가 별로 없고, 패키지 문서화도 조금은 부족한 느낌이라서 헤맬 수도 있을 것 같습니다.

이 문서에서는 간단하게 drf-yasg 적용방법과 쿼리 파라미터, 응답에 대한 문서화 방법을 다루겠습니다.

제가 이해한대로 작성한 것이니 다르거나 틀린 부분이 있을 수도 있습니다. 또 패키지 성격상 적용하시는 개발자/팀의 컨벤션에 따라 자유롭게 사용할 수 있을 것 같습니다.

시작하기 전, 예제 프로젝트 설명

저는 예제 django project mysite를 만들고, 내부에 app myapp을 생성했습니다.

  • 프로젝트의 뷰셋들은 drf의 Router에 등록되어 있습니다.
  • 프로젝트에는 drf의 LimitOffsetPagination이 적용된 상태입니다.
  • 아래는 모델과 뷰셋에 대한 코드입니다.

models.py :

from django.db import models


class TestModel1(models.Model):
    type_str = models.CharField(max_length=100)
    type_int = models.IntegerField()
    type_bool = models.BooleanField()


class TestModel2(models.Model):
    type_str = models.CharField(max_length=100)
    type_for = models.ForeignKey(
        'TestModel1',
        on_delete=models.CASCADE
    )


from rest_framework import serializers


class TestModel1Serializer(serializers.ModelSerializer):
    class Meta:
        model = TestModel1
        fields = '__all__'


class TestModel2Serializer(serializers.ModelSerializer):
    class Meta:
        model = TestModel2
        fields = '__all__'

views.py :

from rest_framework import viewsets
from rest_framework.response import Response

from .models import TestModel1, TestModel2, TestModel1Serializer, TestModel2Serializer


class TestModel1ViewSet(viewsets.ModelViewSet):
    queryset = TestModel1.objects.all()
    serializer_class = TestModel1Serializer

    def list(self, request, *args, **kwargs):
        param_hello = request.query_params.get('hello')
        if param_hello:
            # do something with hello...
            queryset = TestModel1.objects.filter(type_int__gt=1)
            queryset = self.paginate_queryset(queryset)
            serializer = self.get_serializer(queryset, many=True)
            return self.get_paginated_response(serializer.data)
        return super().list(request, *args, **kwargs)


class TestModel2ViewSet(viewsets.ModelViewSet):
    queryset = TestModel2.objects.all()
    serializer_class = TestModel2Serializer

    def list(self, request, *args, **kwargs):
        param_world = request.query_params.get('world')

        if param_world:
            # do something with world...
            return Response({
                'error': 'eeeeeeror!',
                'detail': 'something bad happened',
                'code': 10
            }, status=500)
        return super().list(request, *args, **kwargs)

패키지 설치 및 적용

그러면 본격적으로 drf-yasg를 적용해보도록 하겠습니다.

먼저 패키지를 설치합니다.

(venv) $ pip install drf-yasg

그리고 settings.py의 INSTALLED_APPS에 drf-yasg을 추가합니다.

INSTALLED_APPS = [
    ...
    'rest_framework',
    'myapp',
    # drf_yasg를 APPS에 추가
    'drf_yasg'
]

swagger 엔드포인트 추가

이제 swagger 문서를 볼 수 있는 엔드포인트를 추가해야 합니다.

get_schema_view()를 통해 문서 뷰를 가져오고 url을 등록합니다.

In urls.py :

...

from django.conf import settings
from django.urls import path, include, re_path

from rest_framework import routers, permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi

...

# swagger 정보 설정, 관련 엔드포인트 추가
# swagger 엔드포인트는 DEBUG Mode에서만 노출
schema_view = get_schema_view(
    openapi.Info(
        title="Snippets API",
        default_version='v1',
        description="Test description",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@snippets.local"),
        license=openapi.License(name="BSD License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

if settings.DEBUG:
    urlpatterns += [
        re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
        re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
        re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc')
    ]

 

적용이 끝났습니다. 서버를 실행 후 /swagger/에 접속하면 다음 화면이 나타납니다.

문서를 둘러보면 모델의 정의, API 리스트가 잘 나오는 것을 볼 수 있습니다.

ViewSet의 docstring

정의한 뷰셋에 docstring을 추가하면 swagger 문서에서도 나타납니다. Markdown 문법을 지원한다고 합니다.

주의할 점은 ViewSet의 docstring을 사용하면, CRUD 모든 엔드포인트의 윗부분에 추가된다는 점입니다. 이 윗부분을 일반적으로 summary라고 하는 것 같습니다.

In views.py :

class TestModel1ViewSet(viewsets.ModelViewSet):
    """
    Model1의 CRUD
    ---
    Hello, World
    """
    queryset = TestModel1.objects.all()
    serializer_class = TestModel1Serializer
    
    ...

결과 :

추가 문서화 (Custom Schema Generation)

문서를 자세히 보면, 코드 내부에서 정의한 쿼리 파라미터나 에러 응답에 대한 내용은 없습니다.

당연한 결과이겠죠?... drf-yasg가 내부 코드까지 분석해줄 수는 없으니까요 :(

이때 swagger_auto_schema 데코레이터와 openapi.Parameter, openapi.Schema 통해 빠진 부분을 문서화할 수 있습니다.

쿼리 파라미터

먼저 TestModel1ViewSet의 hello 쿼리 파라미터에 대한 설명을 추가해보겠습니다.

In views.py :

...
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema

class TestModel1ViewSet(viewsets.ModelViewSet):
    """
    Model1의 CRUD
    ---
    Hello, World
    """
    queryset = TestModel1.objects.all()
    serializer_class = TestModel1Serializer

    # manual parameter
    param_hello_hint = openapi.Parameter(
        'hello',
        openapi.IN_QUERY,
        description='this is a description of hello.',
        type=openapi.TYPE_STRING
    )

    @swagger_auto_schema(manual_parameters=[param_hello_hint])
    def list(self, request, *args, **kwargs):
        ...

먼저 쿼리 파라미터에 대한 정보를 Parameter 클래스로 생성합니다. 파라미터 이름, 어떤 부분에 속하는지(QUERY, BODY, PATH 등), 파라미터 설명, 어떤 타입인지를 생성자에 제공합니다.

파라미터 클래스에 대한 자세한 구현은 문서를 참고하시면 좋습니다.

그 후 데코레이터를 이용해 manual_parameters에 생성한 파라미터 정보를 넘겨줍니다.

결과 :

Custom Response

다음은 응답에 대한 문서화입니다. TestModel2ViewSet의 경우 코드에서 특수한(?) 에러 응답을 넘기고 있습니다.

하지만 drf-yasg는 응답 200, 페이지네이션된 응답만을 알고 있을 것입니다.

응답 문서화 전 (TestModel2ViewSet의 Response 항목) :

500에 대한 응답도 넣어보겠습니다.

In views.py :

class TestModel2ViewSet(viewsets.ModelViewSet):
    """
    Model2의 CRUD
    ---
    Hello, World
    """
    queryset = TestModel2.objects.all()
    serializer_class = TestModel2Serializer

    # manual parameter
    param_world_hint = openapi.Parameter(
        'world',
        openapi.IN_QUERY,
        description='this is a description for world.',
        type=openapi.TYPE_INTEGER
    )

    # custom response schema
    error_field = openapi.Schema(
        'error',
        description='this is a error string.',
        type=openapi.TYPE_STRING
    )
    detail_field = openapi.Schema(
        'detail',
        description='this is a detail string.',
        type=openapi.TYPE_STRING
    )
    code_field = openapi.Schema(
        'code',
        description='this is a code number.',
        type=openapi.TYPE_INTEGER
    )
    error_resp = openapi.Schema(
        'response',
        type=openapi.TYPE_OBJECT,
        properties={
            'error': error_field,
            'detail': detail_field,
            'code': code_field
        }
    )

    @swagger_auto_schema(
        manual_parameters=[param_world_hint],
        responses={
            # can use schema or text
            400: 'this is a test description.',
            500: error_resp
        }
    )
    def list(self, request, *args, **kwargs):
        ...

앞의 파라미터 처리부분보다는 길지만 원리는 간단합니다.

필요한 필드(Schema)를 만들고, 이를 적절하게 빌드하는 방식입니다. Schema 역시 Parameter 클래스와 비슷한 인자를 가지고 있습니다.

Schema에 대한 구현은 문서를 참고하시면 되겠습니다.

데코레이터의 responses에는 dict type을 받으며, key는 status code, value는 응답과 관련된 정보입니다. value에는 일반 텍스트, Schema, Serializer가 들어갈 수 있습니다.

결과 :

특수한 경우: 하나의 ViewSet에서 여러개의 QuerySet/Model을 다룰때 응답 문서화하기

뷰에 아래와 같은 새 뷰셋을 추가했습니다.

In views.py :

class MyViewSet(viewsets.GenericViewSet):
    @action(detail=False)
    def model1(self, request):
        # do something...
        queryset = self.paginate_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return self.get_paginated_response(serializer.data)

    @action(detail=False)
    def model2(self, request):
        # do something...
        queryset = self.paginate_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return self.get_paginated_response(serializer.data)

역시 yasg가 Response가 무엇인지 판단할 수 없을텐데요.

당장 생각나는 방법으로는 @swagger_auto_schema(responses={200: TestModel1Serializer(many=True)}) 를 사용하여 응답 스키마를 지정하는 방법이 있습니다.

하지만 페이지네이션을 사용하고 있다면 위 방법이 문제가 있다는 점을 알 수 있는데요. 바로 count, next 등 페이지네이션 관련 필드가 모두 제외된 채, 단순 Model의 Array로만 문서화됩니다.

이때는 queryset과 serializer을 동적으로 결정해주면 해결할 수 있습니다.

In views.py :

class MyViewSet(viewsets.GenericViewSet):
    queryset = {
        'model1': TestModel1.objects.all(),
        'model2': TestModel2.objects.all()
    }

    serializer_classes = {
        'model1': TestModel1Serializer,
        'model2': TestModel2Serializer
    }

    def get_queryset(self):
        if self.action in self.queryset:
            return self.queryset[self.action]
        return super().get_queryset()

    def get_serializer_class(self):
        if self.action in self.serializer_classes:
            return self.serializer_classes[self.action]
        return super().get_serializer_class()

    @action(detail=False)
    def model1(self, request):
        # do something...
        queryset = self.paginate_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return self.get_paginated_response(serializer.data)

    @action(detail=False)
    def model2(self, request):
        # do something...
        queryset = self.paginate_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        return self.get_paginated_response(serializer.data)

페이지네이션이 적용된 스키마를 볼 수 있네요 :)

 

 

 

 

출처: velog.io/@rubycho/%EB%AC%B8%EC%84%9C%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-drf-yasg-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 


## drf-yasg에 예제값 달기

https://pypi.org/project/drf-yasg-examples/

최근 저는 Django를 이용해서 API 프로토타입을 만들고 있습니다. Django와 django-rest-framework을 이용해서 Web API를 만들고, 그것에 대해 drf-yasg를 통해 API 문서화를 자동화 하려고 했죠.

 

 

예시가 이상해

그런데, 문제가 나타났습니다. drf-yasg가 만들어주는 문서 예제가 너무나도 쓸모가 없다는 점이었습니다.

분명히 name_en 필드에 와야하는 값이 string 자료형이어야하는 것은 맞지만 "string" 이란 값을 기대하는 필드는 단언코 아닙니다. 저는 사람의 이름을 적기를 기대하고 있었기에 지금 상태로는 곤란했죠

없으면 만든다

그런데, drf-yasg에는 이걸 해결할 방법을 제공해주지 않고 있었습니다. 고민끝에, 직접 만들어보기로 했습니다.

 

 

1단계. 어떻게 접근할지 정한다.

다행히도 drf-yasg의 문서에는 Inspector를 직접 만드는 방법을 소개하고 있었습니다.1 Inspector를 통해 원하는 필드를 추가하거나 제거할 수 있었죠. 이제 문제는 예시값을 추가할 방법입니다. Swagger문서에서 예제값이 필요한 지점은 Serializer의 각 field입니다. 즉, Serializer를 문서로 만드는 단계에서 field별로 설명을 추가하면 되는 것입니다. 하지만 serializer field를 개조하는 것은 너무 돌아가는 접근법 같아서 간단한 접근법을 구상했습니다. 이미 사용되는 Meta class를 사용하는 것입니다.

아래와 같이 사용하는 것을 가정했습니다. (아래 코드는 예시입니다.)

class PersonSerializer(serializers.ModelSerializer):

    class Meta:
        model = Person
        exclude = ['created_at', 'updated_at', 'deleted_at']
        examples = {
            'id': 1,
            'name_en': 'kirito',
            'name_ja': 'キリト',
            'name_ko': '키리토',
            'name_zh_cn': '桐人',
            'name_zh_tw': '桐人',
            'priority': 1,
            'gender': Gender.MALE,
        }

각 serializer의 Meta 클래스 안에 examples 라는 dict를 생성해두면 해당 값을 가져다가 문서에 넣는 것이죠.

 

 

2단계. Inspector 만들기

API 명세가 정해졌으므로 구현하기만 하면 됩니다. 저는 아래와 같이 구현했습니다.

from drf_yasg import openapi
from drf_yasg.inspectors import SerializerInspector


class ExampleInspector(SerializerInspector):
    def process_result(self, result, method_name, obj, **kwargs):
        # obj.Meta.examples 에 접근할 수 없다면 예시를 넣을 수 없습니다.
        has_examples = hasattr(obj, 'Meta') and hasattr(obj.Meta, 'examples')
        if isinstance(result, openapi.Schema.OR_REF) and has_examples:
            schema = openapi.resolve_ref(result, self.components)
            # properties가 정의되지 않은 경우엔 할 수 있는게 없습니다.
            if 'properties' in schema:
                properties = schema['properties']
                for name in properties.keys():
                    # 예시를 정해둔 필드만 손댑니다.
                    if name in obj.Meta.examples:
                        properties[name]['example'] = obj.Meta.examples[name]

        # schema를 return하면 안 됩니다.
        # 위에서 schema를 수정해도 reference되어서 result에 반영됩니다.
        return result

 

 

3단계. drf-yasg에 연결하기

Inspector를 새로 만들었으니 drf-yasg에 연결하고 실제 사용해봐야합니다. 그러려면 새로운 SwaggerAutoSchema를 정의해야합니다.

from drf_yasg.utils import swagger_settings
from drf_yasg.inspectors import SwaggerAutoSchema


class MyAutoSchema(SwaggerAutoSchema):

    field_inspectors = [
        ExampleInspector,
    ] + swagger_settings.DEFAULT_FIELD_INSPECTORS

사용법은 두 가지가 있습니다.

하나. 특정 ViewSet에만 적용

from proj.inspectors import MyAutoSchema

class PersonViewSet(viewsets.ModelViewSet):

    queryset = Person.objects.filter(deleted_at__isnull=True)
    serializer_class = PersonSerializer
    swagger_schema = MyAutoSchema  # 추가

이렇게 하면 특정 ViewSet에만 적용이 가능합니다.

둘. 모든 View에 적용

제 경우는 모든 View에 적용하길 원했기에, 아예 기본 설정값을 갈아엎길 원했습니다. settings.py 파일에 다음과 같이 추가하면 됩니다.

SWAGGER_SETTINGS = {
    'DEFAULT_AUTO_SCHEMA_CLASS': 'myproj.inspectors.MyAutoSchema',
}

 

 

결과물 보기

이제 예시값이 현실에서 실제 사용될법한 값이 되었으므로 문서를 보게 될 분들도 혼란이 덜 할것입니다. 이 예시값은 요청 예시 뿐만 아니라 결과 예시나 try it now에서도 사용되므로 매우 편리합니다.

 

 

마치며

drf-yasg는 매우 강력한 문서화 도구이지만 완벽하다고 말하기엔 부족한 점들이 있습니다. 이런 문제는 Inspector를 추가 정의하면 보다 효율적으로 개선이 가능합니다.

 

 

2020년 3월 22일 추가

이 기능을 쉽게 사용할 수 있도록 drf-yasg-examples라는 패키지를 만들었습니다. 단순히 예제 기능만 필요한 것이라면 이쪽을 사용하는 것이 더 간단합니다!

https://drf-yasg.readthedocs.io/en/stable/custom_spec.html

 

 

 

 

출처: item4.blog/2020-03-04/Add-Example-on-drf-yasg/

 

+ Recent posts