Skip to content

Latest commit

 

History

History
400 lines (292 loc) · 18.8 KB

custom_spec.rst

File metadata and controls

400 lines (292 loc) · 18.8 KB

Custom schema generation

If the default spec generation does not quite match what you were hoping to achieve, drf-yasg provides some custom behavior hooks by default.

Swagger spec overview

This library generates OpenAPI 2.0 documents. The authoritative specification for this document's structure will always be the official documentation over at swagger.io and the OpenAPI 2.0 specification page.

Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification is structured, starting from the root Swagger object.

  • :class:`.Swagger` object
    • info, schemes, securityDefinitions and other informative attributes
    • paths: :class:`.Paths` object
      A list of all the paths in the API in the form of a mapping
    • definitions: named Models
      A list of all the named models in the API in the form of a mapping
  • :class:`.Operation` contains the following information about each operation:
    • parameters: [:class:`.Parameter`]
      A list of all the query, header and form parameters accepted by the operation.
    • responses: :class:`.Responses`
      A list of all the possible responses the operation is expected to return. Each response can optionally have a :class:`.Schema` which describes the structure of its body.
    • operationId - should be unique across all operations
    • tags - used to group operations in the listing

It is interesting to note the main differences between :class:`.Parameter` and :class:`.Schema` objects:

:class:`.Schema` :class:`.Parameter`
Can nest other Schemas Cannot nest other Parameters
Can only nest a Schema if the parameter is in: body
Cannot describe file uploads
- file is not permitted as a value for type
Can describe file uploads via type = file,
but only as part of a form :class:`.Operation` [1]
Can be used in :class:`.Response`s Cannot be used in :class:`.Response`s
Cannot be used in form :class:`.Operation`s [1] Can be used in form :class:`.Operation`s [1]
Can only describe request or response bodies Can describe query, form, header or path parameters
[1](1, 2, 3)

a form Operation is an :class:`.Operation` that consumes multipart/form-data or application/x-www-form-urlencoded content

  • a form Operation cannot have body parameters
  • a non-form operation cannot have form parameters

Default behavior

This section describes where information is sourced from when using the default generation process.

The @swagger_auto_schema decorator

You can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator on view functions to override some properties of the generated :class:`.Operation`. For example, in a ViewSet,

@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
def partial_update(self, request, *args, **kwargs):
   """partial_update method docstring"""
   ...

will override the description of the PATCH /article/{id}/ operation, and document a 404 response with no body and the given description.

Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator depends on the type of your view:

  • for function based @api_views, because the same view can handle multiple methods, and thus represent multiple operations, you have to add the decorator multiple times if you want to override different operations:

    test_param = openapi.Parameter('test', openapi.IN_QUERY, description="test manual param", type=openapi.TYPE_BOOLEAN)
    user_response = openapi.Response('response description', UserSerializer)
    
    # 'method' can be used to customize a single HTTP method of a view
    @swagger_auto_schema(method='get', manual_parameters=[test_param], responses={200: user_response})
    # 'methods' can be used to apply the same modification to multiple methods
    @swagger_auto_schema(methods=['put', 'post'], request_body=UserSerializer)
    @api_view(['GET', 'PUT', 'POST'])
    def user_detail(request, pk):
        ...
  • for class based APIView, GenericAPIView and non-ViewSet derivatives, you have to decorate the respective method of each operation:

    class UserList(APIView):
       @swagger_auto_schema(responses={200: UserSerializer(many=True)})
       def get(self, request):
          ...
    
       @swagger_auto_schema(operation_description="description")
       def post(self, request):
          ...
  • for ViewSet, GenericViewSet, ModelViewSet, because each viewset corresponds to multiple paths, you have to decorate the action methods, i.e. list, create, retrieve, etc.
    Additionally, @list_routes or @detail_routes defined on the viewset, like function based api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:

    class ArticleViewSet(viewsets.ModelViewSet):
       # method or 'methods' can be skipped because the list_route only handles a single method (GET)
       @swagger_auto_schema(operation_description='GET /articles/today/')
       @list_route(methods=['get'])
       def today(self, request):
          ...
    
       @swagger_auto_schema(method='get', operation_description="GET /articles/{id}/image/")
       @swagger_auto_schema(method='post', operation_description="POST /articles/{id}/image/")
       @detail_route(methods=['get', 'post'], parser_classes=(MultiPartParser,))
       def image(self, request, id=None):
          ...
    
       @swagger_auto_schema(operation_description="PUT /articles/{id}/")
       def update(self, request, *args, **kwargs):
          ...
    
       @swagger_auto_schema(operation_description="PATCH /articles/{id}/")
       def partial_update(self, request, *args, **kwargs):
          ...

Tip

If you want to customize the generation of a method you are not implementing yourself, you can use swagger_auto_schema in combination with Django's method_decorator:

@method_decorator(name='list', decorator=swagger_auto_schema(
    operation_description="description from swagger_auto_schema via method_decorator"
))
class ArticleViewSet(viewsets.ModelViewSet):
   ...

This allows you to avoid unnecessarily overriding the method.

Tip

You can go even further and directly decorate the result of as_view, in the same manner you would override an @api_view as described above:

decorated_login_view = \
   swagger_auto_schema(
      method='post',
      responses={status.HTTP_200_OK: LoginResponseSerializer}
   )(LoginView.as_view())

urlpatterns = [
   ...
   url(r'^login/$', decorated_login_view, name='login')
]

This can allow you to avoid skipping an unnecessary subclass altogether.

Warning

However, do note that both of the methods above can lead to unexpected (and maybe surprising) results by replacing/decorating methods on the base class itself.

Serializer Meta nested class

You can define some per-serializer options by adding a Meta class to your serializer, e.g.:

class WhateverSerializer(Serializer):
   ...

   class Meta:
      ... options here ...

Currently, the only option you can add here is

  • ref_name - a string which will be used as the model definition name for this serializer class; setting it to None will force the serializer to be generated as an inline model everywhere it is used

Subclassing and extending

SwaggerAutoSchema

For more advanced control you can subclass :class:`~.inspectors.SwaggerAutoSchema` - see the documentation page for a list of methods you can override.

You can put your custom subclass to use by setting it on a view method using the :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` decorator described above, by setting it as a class-level attribute named swagger_schema on the view class, or :ref:`globally via settings <default-class-settings>`.

For example, to generate all operation IDs as camel case, you could do:

from inflection import camelize

class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema):
   def get_operation_id(self, operation_keys):
      operation_id = super(CamelCaseOperationIDAutoSchema, self).get_operation_id(operation_keys)
      return camelize(operation_id, uppercase_first_letter=False)


SWAGGER_SETTINGS = {
   'DEFAULT_AUTO_SCHEMA_CLASS': 'path.to.CamelCaseOperationIDAutoSchema',
   ...
}

OpenAPISchemaGenerator

If you need to control things at a higher level than :class:`.Operation` objects (e.g. overall document structure, vendor extensions in metadata) you can also subclass :class:`.OpenAPISchemaGenerator` - again, see the documentation page for a list of its methods.

This custom generator can be put to use by setting it as the :attr:`.generator_class` of a :class:`.SchemaView` using :func:`.get_schema_view`.

Inspector classes

.. versionadded:: 1.1

For customizing behavior related to specific field, serializer, filter or paginator classes you can implement the :class:`~.inspectors.FieldInspector`, :class:`~.inspectors.SerializerInspector`, :class:`~.inspectors.FilterInspector`, :class:`~.inspectors.PaginatorInspector` classes and use them with :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` or one of the :ref:`related settings <default-class-settings>`.

A :class:`~.inspectors.FilterInspector` that adds a description to all DjangoFilterBackend parameters could be implemented like so:

class DjangoFilterDescriptionInspector(CoreAPICompatInspector):
   def get_filter_parameters(self, filter_backend):
      if isinstance(filter_backend, DjangoFilterBackend):
         result = super(DjangoFilterDescriptionInspector, self).get_filter_parameters(filter_backend)
         for param in result:
            if not param.get('description', ''):
               param.description = "Filter the returned list by {field_name}".format(field_name=param.name)

         return result

      return NotHandled

@method_decorator(name='list', decorator=swagger_auto_schema(
   filter_inspectors=[DjangoFilterDescriptionInspector]
))
class ArticleViewSet(viewsets.ModelViewSet):
   filter_backends = (DjangoFilterBackend,)
   filter_fields = ('title',)
   ...

A second example, of a :class:`~.inspectors.FieldInspector` that removes the title attribute from all generated :class:`.Schema` objects:

class NoSchemaTitleInspector(FieldInspector):
   def process_result(self, result, method_name, obj, **kwargs):
      # remove the `title` attribute of all Schema objects
      if isinstance(result, openapi.Schema.OR_REF):
         # traverse any references and alter the Schema object in place
         schema = openapi.resolve_ref(result, self.components)
         schema.pop('title', None)

         # no ``return schema`` here, because it would mean we always generate
         # an inline `object` instead of a definition reference

      # return back the same object that we got - i.e. a reference if we got a reference
      return result


class NoTitleAutoSchema(SwaggerAutoSchema):
   field_inspectors = [NoSchemaTitleInspector] + swagger_settings.DEFAULT_FIELD_INSPECTORS

class ArticleViewSet(viewsets.ModelViewSet):
   swagger_schema = NoTitleAutoSchema
   ...

Note

A note on references - :class:`.Schema` objects are sometimes output by reference (:class:`.SchemaRef`); in fact, that is how named models are implemented in OpenAPI:

  • in the output swagger document there is a definitions section containing :class:`.Schema` objects for all models
  • every usage of a model refers to that single :class:`.Schema` object - for example, in the ArticleViewSet above, all requests and responses containg an Article model would refer to the same schema definition by a '$ref': '#/definitions/Article'

This is implemented by only generating one :class:`.Schema` object for every serializer class encountered.

This means that you should generally avoid view or method-specific FieldInspectors if you are dealing with references (a.k.a named models), because you can never know which view will be the first to generate the schema for a given serializer.