axnsan12 / drf-yasg

Automated generation of real Swagger/OpenAPI 2.0 schemas from Django REST Framework code.
https://drf-yasg.readthedocs.io/en/stable/
Other
3.37k stars 433 forks source link

Provide a django-rest-framework-json-api integration #92

Open cocochepeau opened 6 years ago

cocochepeau commented 6 years ago

Hello,

I'm pretty new to the python/django world so please forgive me in advance for my mistakes!

I wanna use drf-yasg with django-rest-framework-json-api. The "default" installation seems to work as expected - except for one little thing, the swagger response samples are not accurate.

For example, this is what's currently provided in the Swagger UI response sample for http://localhost:8080/v1/entities/ (I guess this is the default drf response schema):

{
  "count": 0,
  "next": "string",
  "previous": "string",
  "results": [
    {
      "id": 0,
      "name": "string"
    }
  ]
}

What I expect (more like json api spec):

{
  "links": {
    "first": "http://localhost:8080/v1/entities/?page=1",
    "last": "http://localhost:8080/v1/entities/?page=2",
    "next": "http://localhost:8080/v1/entities/?page=2",
    "prev": null
  },
  "data": [
    {
      "type": "entities",
      "id": "1",
      "attributes": {
        "name": "xxx"
      }
    },
    ...
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pages": 2,
      "count": 19
    }
  }
}

For information, here is what my renderer classes settings looks like:

REST_FRAMEWORK = {

    ...

    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework_json_api.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer'
    ),

    ...
}

My question is: How can I achieve such thing?

Thanks!

axnsan12 commented 6 years ago

Assuming you configured drf-json-api as outlined here, you could probably get it done by modifying the response using a PaginatorInspector as described here.

Otherwise, for a more general implementation you could modify responses by overriding SwaggerAutoSchema.get_response_schemas.

If you do manage to get it working, I wouldn't mind merging a pull request for an integration with django-rest-framework-json-api 😄

cocochepeau commented 6 years ago

Hi again,

I made some more research. It appears that Swagger / OA2 doesn't fully support the JSON API spec (or in other words, the two are different). I'm not sure if that's even a good idea to try something or if it sounds right?

still learning.

axnsan12 commented 6 years ago

I'm not sure I see how the two specs relate. JSON API is a way of structuring JSON bodies of HTTP requests and responses of an API, while Swagger is a way of programatically describing the structure of said response (a meta-API, if you will).

The only part of JSON API that couldn't really be represented by Swagger is the hyperlinkling part (i.e. link.first is a URL vs links.first is a URL that points to a page of entities). The request/response structure of individual endpoints, though, should be fully describable using Swagger as far as I can tell.

kujbol commented 6 years ago

@cocochepeau hey you can write your own field and pagination Inspectors something similar to this: You can probably write it way better, but it is working and doing it's work

from rest_framework_json_api import serializers

class ResourceRelatedFieldInspector(FieldInspector):
    def field_to_swagger_object(
            self, field, swagger_object_type, use_references, **kwargs
    ):
        if isinstance(field, serializers.ResourceRelatedField):
            return None

        return NotHandled

class ModelSerializerInspector(FieldInspector):
    def process_result(self, result, method_name, obj, **kwargs):
        if (
            isinstance(obj, serializers.ModelSerializer) and
            method_name == 'field_to_swagger_object'
        ):
            model_response = self.formatted_model_result(result, obj)
            if obj.parent is None and self.view.action != 'list':
                # It will be top level object not in list, decorate with data
                return self.decorate_with_data(model_response)

            return model_response

        return result

    def generate_relationships(self, obj):
        relationships_properties = []
        for field in obj.fields.values():
            if isinstance(field, serializers.ResourceRelatedField):
                relationships_properties.append(
                    self.generate_relationship(field)
                )
        if relationships_properties:
            return openapi.Schema(
                title='Relationships of object',
                type=openapi.TYPE_OBJECT,
                properties=OrderedDict(relationships_properties),
            )

    def generate_relationship(self, field):
        field_schema = openapi.Schema(
            title='Relationship object',
            type=openapi.TYPE_OBJECT,
            properties=OrderedDict((
                ('type', openapi.Schema(
                    type=openapi.TYPE_STRING,
                    title='Type of related object',
                    enum=[get_related_resource_type(field)]
                )),
                ('id', openapi.Schema(
                    type=openapi.TYPE_STRING,
                    title='ID of related object',
                ))
            ))
        )
        return field.field_name, self.decorate_with_data(field_schema)

    def formatted_model_result(self, result, obj):
        return openapi.Schema(
            type=openapi.TYPE_OBJECT,
            required=['properties'],
            properties=OrderedDict((
                ('type', openapi.Schema(
                    type=openapi.TYPE_STRING,
                    enum=[get_resource_type_from_serializer(obj)],
                    title='Type of related object',
                )),
                ('id', openapi.Schema(
                    type=openapi.TYPE_STRING,
                    title='ID of related object',
                    read_only=True
                )),
                ('attributes', result),
                ('relationships', self.generate_relationships(obj))
            ))
        )

    def decorate_with_data(self, result):
        return openapi.Schema(
            type=openapi.TYPE_OBJECT,
            required=['data'],
            properties=OrderedDict((
                ('data', result),
            ))
        )

and pagination:

class DjangoRestJsonApiResponsePagination(PaginatorInspector):
    def get_paginator_parameters(self, paginator):
        return [
            openapi.Parameter(
                'limit', in_=IN_QUERY, type=openapi.TYPE_INTEGER
            ),
            openapi.Parameter(
                'offset', in_=IN_QUERY, type=openapi.TYPE_INTEGER
            ),
        ]

    def get_paginated_response(self, paginator, response_schema):
        paged_schema = None
        if isinstance(paginator, LimitOffsetPagination):
            paged_schema = openapi.Schema(
                type=openapi.TYPE_OBJECT,
                properties=OrderedDict((
                    ('links', self.generate_links()),
                    ('data', response_schema),
                    ('meta', self.generate_meta())
                )),
                required=['data']
            )

        return paged_schema

    def generate_links(self):
        return openapi.Schema(
            title='Links',
            type=openapi.TYPE_OBJECT,
            required=['first', 'last'],
            properties=OrderedDict((
                ('first', openapi.Schema(
                    type=openapi.TYPE_STRING, title='Link to first object',
                    read_only=True, format=openapi.FORMAT_URI
                )),
                ('last', openapi.Schema(
                    type=openapi.TYPE_STRING, title='Link to last object',
                    read_only=True, format=openapi.FORMAT_URI
                )),
                ('next', openapi.Schema(
                    type=openapi.TYPE_STRING, title='Link to next object',
                    read_only=True, format=openapi.FORMAT_URI
                )),
                ('prev', openapi.Schema(
                    type=openapi.TYPE_STRING, title='Link to prev object',
                    read_only=True, format=openapi.FORMAT_URI
                ))
            ))
        )

    def generate_meta(self):
        return openapi.Schema(
            title='Meta of result with pagination count',
            type=openapi.TYPE_OBJECT,
            required=['count'],
            properties=OrderedDict((
                ('count', openapi.Schema(
                    type=openapi.TYPE_INTEGER,
                    title='Number of results on page',
                )),
            ))
        )
cocochepeau commented 6 years ago

Hi,

Sorry for the late answer.

@axnsan12 Indeed, you're right. @kujbol Thanks for sharing this. I'll try in the coming days.

glowka commented 4 years ago

Hey!

I have just released package integrating django-rest-framework-json-api with drf-yasg. It's based on my code that I have been using for some time in couple of projects. Easily integrable into the codebase if you already use drf-yasg.

You can see it at https://github.com/glowka/drf-yasg-json-api

BTW Since it's the very first time I write anything in this repo: I'd like to thanks you @axnsan12 for development of this very nice tool which drf-yasg absolutely is!