drf_yasg : REST 프레임워크용 swagger/openAPI 문서 자동화 라이브러리 swagger : 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 테스트를 도와주는 오픈소스 소프트웨어 프레임워크

drf_yasg : 1.20.0 호환버전

  • Django Rest Framework : 3.10, 3.11, 3.12
  • Django : 2.2, 3.0, 3.1
  • Python : 3.6, 3.7, 3.8, 3.9

라이브러리 설치

pip install -U drf-yasg

1. 프로젝트 생성

환경 : windwos 10

# 가상환경 생성 및 접속
> python -m venv venv
> venv\Scripts\activate

# 라이브러리 설치
(venv)> pip install django
(venv)> pip install djangorestframework
(venv)> pip install drf-yasg

# 프로젝트 및 앱 생성
(venv)> django-admin startproject config .
(venv)> django-admin startapp api

2. django 설정

config/settings.py

staticfiles앱은 swagger UI css/js 파일 배포용

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'drf_yasg',
    ...
]

config/urls.py

...
from rest_framework.permissions import AllowAny 
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from config import settings
...

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(name="tester", email="test@test.com"),
      license=openapi.License(name="BSD License"),
   ),
   public=True,
   permission_classes=(AllowAny,),
)
...

# debug 모드일때 swagger api 실행
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'),
        ...
    ]

3. 예시코드

3.1. models

api/models.py

from django.db import models

# Create your models here.

class Musics(models.Model):
    ONE_STAR = 1
    TWO_STAR = 2
    THREE_STAR = 3
    STARS = (
        (ONE_STAR, '보통'),
        (TWO_STAR, '좋음'),
        (THREE_STAR, '매우 좋음'),
    )

    CATEGORY = (
        ('KPOP', '케이팝'),
        ('POP', '팝송'),
        ('CLASSIC', '클래식'),
        ('ETC', '기타 등등'),
    )

    id = models.BigAutoField(primary_key=True, verbose_name='music_id')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='생성 날짜')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='수정 날짜')
    deleted_at = models.DateTimeField(blank=True, null=True, verbose_name='삭제 날짜')
    singer = models.CharField(null=False, max_length=128, verbose_name='가수명')
    title = models.CharField(null=False, max_length=128, verbose_name='곡명')
    category = models.CharField(blank=True, null=True, max_length=32, choices=CATEGORY, verbose_name='범주')
    star_rating = models.PositiveSmallIntegerField(blank=True, null=True, choices=STARS, default=ONE_STAR, verbose_name='곡 선호도')

    class Meta:
        # abstract = True  # sqlite3 사용 시 주석
        managed = True
        db_table = 'musics'
        app_label = 'api'
        ordering = ['star_rating', ]
        verbose_name_plural = '음악'

DB 마이그레이션

(venv)> py manage.py makemigrations
(venv)> py manage.py migrate

생성된 sqlite3 파일을 확인할려면 아래 링크에서 프로그램을 다운받아 설치한다. 개인적으로 no installer(portable) 버전을 추천한다.

db.sqlite3

musics 테이블 생성된 것을 확인할 수 있다.

3.2. seializers

api/serializers.py

from rest_framework import serializers

from .models import Musics


class MusicSerializer(serializers.ModelSerializer):
    def validate(self, data: dict):
        return data

    class Meta:
        model = Musics
        fields = '__all__'  # 모든 필드 사용
        # fields = ('id', 'created_at', 'title', 'category', 'star_rating',)  # 특정 필드 지정

class MusicBodySerializer(serializers.Serializer):
    singer = serializers.CharField(help_text="가수명")
    title = serializers.CharField(help_text="곡 제목")
    category = serializers.ChoiceField(help_text="곡 범주", choices=('KPOP', 'POP', 'CLASSIC', 'ETC'))
    star_rating = serializers.ChoiceField(help_text="1~3 이내 지정 가능. 숫자가 클수록 좋아하는 곡", choices=(1, 2, 3), required=False)

class MusicQuerySerializer(serializers.Serializer):
    title = serializers.CharField(help_text="곡 제목으로 검색", required=False)
    star_rating = serializers.ChoiceField(help_text="곡 선호도로 검색", choices=(1, 2, 3), required=False)
    singer = serializers.CharField(help_text="가수명으로 검색", required=True)
    category = serializers.ChoiceField(help_text="카테고리로 검색", choices=('KPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    created_at = serializers.DateTimeField(help_text="입력한 날짜를 기준으로 그 이전에 추가된 곡들을 검색", required=False)

3.3. class views

api/views.py

from django.shortcuts import get_object_or_404

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response 
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema

from .serializers import *

class MusicListView(APIView):
    def get(self, request):
        serializer = MusicSerializer(Musics.objects.all(), many=True)

        response = Response(data=serializer.data)
        return response

    @swagger_auto_schema(request_body=MusicBodySerializer)  # Request Serializer 지정
    def post(self, request):
        # 중복 검사 없이 추가
        # serializer = MusicSerializer(data=request.data)
        
        # if not serializer.is_valid(raise_exception=False):
        #     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)  

        # serializer.save()
        # response = Response(data=serializer.validated_data, status=status.HTTP_201_CREATED)
        # return response
        
        # 중복 검사 후 추가
        musics = Musics.objects.filter(**request.data)
        if musics.exists():
            return Response(status=status.HTTP_406_NOT_ACCEPTABLE)

        serializer = MusicSerializer(data=request.data, partial=True)
        if not serializer.is_valid():
            return Response(status=status.HTTP_406_NOT_ACCEPTABLE)

        music = serializer.save()

        response = Response(data=MusicSerializer(music).data, status=status.HTTP_201_CREATED)
        return response


class SearchMusicListView(APIView):
    @swagger_auto_schema(query_serializer=MusicQuerySerializer)
    def get(self, request):
        # filter 조건문 생성
        conditions = {
            'title__contains': request.GET.get('title', None),
            'star_rating': request.GET.get('star_rating', None),
            'singer__contains': request.GET.get('singer', None),
            'category': request.GET.get('category', None),
            'created_at__lte': request.GET.get('created_at', None),
        }
        conditions = {
            key: val for key, val in conditions.items() if val is not None
        }

        musics = Musics.objects.filter(**conditions)
        serializer = MusicSerializer(musics, many=True)
        response = Response(data=serializer.data, status=status.HTTP_200_OK)
        return response


class MusicView(APIView): 
    def get_object(self, pk):
        return get_object_or_404(Musics, pk=pk)

    def get(self, request, pk):
        serializer = MusicSerializer(Musics.objects.filter(id=pk), many=True)

        response = Response(data=serializer.data, status=status.HTTP_200_OK)
        return response

    @swagger_auto_schema(
        request_body=MusicBodySerializer,  # Request Serializer 지정
        manual_parameters=[openapi.Parameter(
            name='header_test',  # api 파라미터 이름
            in_=openapi.IN_HEADER,  # 파라미터 종류 (header = openapi.IN_HEADER)
            description="a header for test",  # 파라미터 설명
            type=openapi.TYPE_STRING  # header 지정시 필수 선언
        )],
    )
    def put(self, request, pk):
        object = self.get_object(pk=pk)
        serializer = MusicSerializer(object, data=request.data)

        if not serializer.is_valid(raise_exception=False):
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

        serializer.save()
        response = Response(data=serializer.validated_data, status=status.HTTP_200_OK)
        return response

    def delete(self, request, pk):
        object = self.get_object(pk=pk)
        object.delete()

        response = Response(status=status.HTTP_204_NO_CONTENT)
        return response

3.4. urls

관리의 편의성을 위해 각 앱별로 urls.py 설정한다. 앱 내부에서 urls 설정한 경우 프로젝트 urls 설정도 같이 해줘야 한다.

api/urls.py

from django.urls import path 
from .views import MusicListView, SearchMusicListView, MusicView, PlayListView, SearchPlayListView

urlpatterns = [ 
    path("music/", MusicListView.as_view(), name="musics"),
    path("music/search/", SearchMusicListView.as_view(), name="music_search"),
    path("music/<int:pk>/", MusicView.as_view(), name="music"),
]

config/urls.py

...
urlpatterns = [
    ...
    path("api/", include("api.urls")),
]
...

swagger 실행후 웹브라우저에서 localhost:8000/swagger/ 주소로 접속한다.

(venv)> py manage.py runserver

각 url class view에서 지정한 메서드들이 표시된다.


4. Swagger API request 지정

swagger api 메소드에서 특정 입력값을 받기 위해서는 @swagger_auto_schema 데코레이터를 지정해야한다.

4.1. query_serializer

  • query string 지정
  • GET, DELETE Method 용도
  • 메소드 입력값을 request.query_params, request.GET로 받는다.
  • request 데이터를 변경하거나 추가할 경우 copy() 함수로 복사해서 사용해야 한다.
# api/views.py 코드
@swagger_auto_schema(query_serializer=MusicQuerySerializer)
def get(self, request):
    data = request.query_params.copy()
    ...
  • Serializer는 변수 선언을 통해서 다양한 타입의 변수를 선언할 수 있다.
  • required 설정을 통해 필수입력, 선택입력을 지정한다.(기본값 True)
# api/serializers.py 코드
class MusicQuerySerializer(serializers.Serializer):
    title = serializers.CharField(help_text="곡 제목으로 검색", required=False)
    star_rating = serializers.ChoiceField(help_text="곡 선호도로 검색", choices=(1, 2, 3), required=False)
    singer = serializers.CharField(help_text="가수명으로 검색", required=True)
    category = serializers.ChoiceField(help_text="카테고리로 검색", choices=('KPOP', 'POP', 'CLASSIC', 'ETC'), required=False)
    created_at = serializers.DateTimeField(help_text="입력한 날짜를 기준으로 그 이전에 추가된 곡들을 검색", required=False)
  • swagger GET 메소드 예시

4.2. request_body

  • body 지정
  • POST, PUT Method 용도
  • 메소드 입력값을 request.data, request.POST로 받는다.
# api/views.py 코드
@swagger_auto_schema(request_body=MusicBodySerializer)
def post(self, request):
    data = request.data.copy()
    ...
  • ModelSerializer는 models.py 에서 지정한 모델의 column을 그대로 사용할 수 있다.
  • Meta 클래스의 fields 설정에 따라 모든 column을 사용하거나 특정 column만 사용할 수 있다.
# api/serializers.py 코드
class MusicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Musics
        fields = '__all__'  # 모든 필드 사용
  • Model 기본키인 id는 id serializer 변수를 직접 지정해야만 사용 가능하다.
# api/serializers.py 코드
class MusicSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField(min_value=1, help_text="ID")

    class Meta:
        model = Musics
        fields = ('id', 'created_at', 'title', 'category', 'star_rating',)  # 응답 필드 지정
  • swagger POST 메소드 예시

4.3. path value

  • path value 지정
  • 모든 메소드에서 사용가능하다.
  • urls에서 지정한 변수명으로 입력값을 받는다.
# api/urls.py
urlpatterns = [ 
    ...
    path("music/<int:pk>/", MusicView.as_view(), name="music"),

class MusicView(APIView): 
    def delete(self, request, pk):
        ...
  • swagger DELETE 메소드 예시


5. Swagger API response 지정

swagger api 메소드에서 특정 입력값 및 반환형식 지정은 @swagger_auto_schema 데코레이터를 사용한다.

5.1. responses

  • swagger 에서 자동으로 HTTP 응답코드에 따른 response 형식을 생성한다.
  • 하지만 상황에 따라 데이터 처리에 사용한 시리얼라이저와 반환값의 형식이 맞지 않는 경우가 생긴다.
  • 이럴때 해당 클래스 메서드에 responses 를 지정하여 HTTP 응답코드에 따른 response 형식을 직접 지정 할 수 있다.
from drf_yasg import openapi
...

music_list_response = openapi.Response('', MusicSerializer(many=True))

class MusicListView(APIView):
    @swagger_auto_schema(responses={200: music_list_response})
    def get(self, request):
        serializer = MusicSerializer(Musics.objects.all(), many=True)

        response = Response(data=serializer.data)
        return response
    ...

responses 적용 전

responses 적용 후

지정한 MusicSerializer 형식의 list 형태로 Swagger Response 형식이 지정된다.

TIP

Swagger Serializer데이터 처리 Serializer를 구분하여 작성하는것이 나중에 유지관리를 위해서 편리하다.

6. 예시 코드 Git


참고(Reference)