🔝

drf-yasgの個人的ベストプラクティス

2024/01/20に公開

drf-yasgについて

DjangoのRESTフレームワークAPIに関してドキュメント自動生成ツールが欲しかったので、色々調べたところ

https://github.com/marcgibbons/django-rest-swagger

This project is no longer being maintained. Please consider drf-yasg as an alternative/successor. I haven't personally used it, but it looks feature-complete and is actively maintained.
https://github.com/axnsan12/drf-yasg

だととのことです。
drf-yasgが推奨ということだったのでこちらを採用することにしました。

ここら辺の用語説明に関しては、GPTにお願いしてまとめてもらいました。

SwaggerとOpenAPIは、APIの設計とドキュメンテーションのための業界標準となっている仕様です。Swaggerは、オープンソースのAPI仕様ツールであり、OpenAPI Specification(以前はSwagger Specificationとして知られていました)を使用してAPIの形式を記述します。一方、OpenAPIはSwaggerの進化形であり、APIの記述やドキュメンテーションに関する包括的な仕様を提供しています。
Django Rest Framework(DRF)は、DjangoベースのWeb APIフレームワークであり、APIの作成や管理を容易にする多くのツールや機能を提供しています。DRFを使用することで、APIの作成と管理が簡単になり、リッチな機能やセキュリティを組み込むことができます。
そして、drf-yasgはDRFの拡張であり、Swagger UIとOpenAPI Specificationを使用して、DRFで構築されたAPIの視覚的なドキュメンテーションを提供します。drf-yasgを利用することで、開発者は簡単かつ効果的にAPIのエンドポイントやリクエスト・レスポンスの仕様を確認し、実際のリクエストを試すことができます。
この組み合わせにより、DRFでAPIを構築する開発者はSwagger UIを通じて直感的で見やすいドキュメンテーションを提供でき、同時にOpenAPI Specificationによって標準的なAPI仕様を満たすことができます。これにより、APIのクライアントや他の開発者がAPIを理解しやすくなり、相互運用性が向上します。

個人的ベストプラクティス

試行錯誤の結果以下に落ち着きました。

def map_drf_to_openapi_type(drf_type):
    """
    Map Django REST framework types to OpenAPI types.
    You may need to extend this mapping based on your specific use case.
    Add more mappings as needed
    """
    type_mapping = {
        serializers.CharField: openapi.TYPE_STRING,
        serializers.IntegerField: openapi.TYPE_INTEGER,
        serializers.FloatField: openapi.TYPE_NUMBER,
        serializers.BooleanField: openapi.TYPE_BOOLEAN,
        serializers.DateField: openapi.TYPE_STRING,
        serializers.DateTimeField: openapi.TYPE_STRING,
    }

    return type_mapping.get(drf_type, openapi.TYPE_STRING)


def generate_swagger_schema(serializer):
    properties = {}
    for field_name, field in serializer().fields.fields.items():
        field_type = map_drf_to_openapi_type(type(field))
        if isinstance(field, serializers.DateField):
            properties[field_name] = openapi.Schema(type=field_type, format='date')
        elif isinstance(field, serializers.DateTimeField):
            properties[field_name] = openapi.Schema(type=field_type, format='date-time')
        else:
            properties[field_name] = openapi.Schema(type=field_type)
    return properties
from generate_swagger_schema import generate_swagger_schema 


class TESTView(APIView):
    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'],
        ),
    ]

    # many = Trueのとき
    success_schema = openapi.Schema(
        type=openapi.TYPE_OBJECT,
        properties={
            "status": openapi.Schema(type=openapi.TYPE_STRING, example="200"),
            "message": openapi.Schema(type=openapi.TYPE_STRING, example="success"),
            "official_blog_detail": openapi.Schema(
                type=openapi.TYPE_ARRAY,
                items=openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties=generate_swagger_schema(OfficialBlogSerializer)
                ),
            ),
        }
    )
    # many = falseのとき 
    success_schema = openapi.Schema(
        type=openapi.TYPE_OBJECT,
        properties={
            "status": openapi.Schema(type=openapi.TYPE_STRING, example="200"),
            "message": openapi.Schema(type=openapi.TYPE_STRING, example="success"),
            "official_blog_detail": openapi.Schema(
                type=openapi.TYPE_OBJECT,
                properties=generate_swagger_schema(OfficialBlogSerializer)
            ),
        }
    )

    error_schema = openapi.Schema(
        type=openapi.TYPE_OBJECT,
        properties={
            "status": openapi.Schema(type=openapi.TYPE_STRING, example="400"),
            "message": openapi.Schema(type=openapi.TYPE_STRING, example="error"),
            "official_blog_detail": openapi.Schema(type=openapi.TYPE_STRING, example=""),
        }
    )

    schema_response = {
        status.HTTP_200_OK: openapi.Response(
            description="成功時のレスポンス",
            schema=success_schema,
        ),
        status.HTTP_400_BAD_REQUEST: openapi.Response(
            description="エラー時のレスポンス",
            schema=error_schema,
        ),
    }

    @swagger_auto_schema(
        manual_parameters=manual_parameters,
        responses=schema_response,
    )
    def get(self, request, **kwargs):
        """
        説明文

        # Markdown

        - 箇条書き
        - の場合
        """
        try:
            context = {
                "status": "200",
                "message": "sucsess",
                'my_info': RetrieveInfomationRelatedUser(request).get_mypage_info(),
            }
        except Exception:
            context = {
                "status": "400",
                "message": "error",
                "my_info": "",
            }
        return Response(context)

UI表示


解説

アーキテクチャとして、Djangoシリアライザを考えた時に、自分はシリアライザdomain/entityに属するべきだと考えています。
つまり、Viewに出てくるべきではないと思ってますし、DB周りの処理を持たせるべきではないと思ってます。
また、ViewにはControllerにresquestを丸投げして、終わりにしたいです。

となると、一般的によくある書き方が通用しません。
シリアライザにステータスコードとメッセージを書くとことも考えましたが、ちょっといけてないなぁっと思いました。
でも、シリアライザのフィールド情報は反映したい。
となって、

class TestView(APIView):
    @swagger_auto_schema(
        responses={
            200: openapi.Response(
                description="成功時のレスポンス",
                schema=openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "status": openapi.Schema(type=openapi.TYPE_STRING, example="200"),
                        "message": openapi.Schema(type=openapi.TYPE_STRING, example="success"),
                        "official_blog_detail": OfficialBlogSerializer
                    }
                )
            ),
        },
    )

としたかったのですが、

TypeError: Object of type SerializerMetaclass is not JSON serializable

に苦しめられて、GPTと相談して行ったところ、マッピングするという結論になりました。

GitHubで編集を提案

Discussion