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.

Excluding endpoints

You can prevent a view from being included in the Swagger view by setting its class-level swagger_schema attribute to None, or you can prevent an operation from being included by setting its auto_schema override to none in @swagger_auto_schema:

class UserList(APIView):
   swagger_schema = None

   # all methods of the UserList class will be excluded
   ...

# only the GET method will be shown in Swagger
@swagger_auto_schema(method='put', auto_schema=None)
@swagger_auto_schema(methods=['get'], ...)
@api_view(['GET', 'PUT'])
def user_detail(request, pk):
   pass

The @swagger_auto_schema decorator

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

from drf_yasg.utils import swagger_auto_schema

@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 @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:

    from drf_yasg import openapi
    from drf_yasg.utils import swagger_auto_schema
    from rest_framework.decorators import api_view
    
    from drf_yasg import openapi
    
    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, @actions 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 action only handles a single method (GET)
       @swagger_auto_schema(operation_description='GET /articles/today/')
       @action(detail=False, 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/")
       @action(detail=True, 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.

Support for SerializerMethodField

Schema generation of serializers.SerializerMethodField is supported in two ways:

  1. The swagger_serializer_method decorator for the use case where the serializer method is using a serializer. e.g.:

    from drf_yasg.utils import swagger_serializer_method
    
    class OtherStuffSerializer(serializers.Serializer):
        foo = serializers.CharField()
    
    class ParentSerializer(serializers.Serializer):
        other_stuff = serializers.SerializerMethodField()
        many_other_stuff = serializers.SerializerMethodField()
    
        @swagger_serializer_method(serializer_or_field=OtherStuffSerializer)
        def get_other_stuff(self, obj):
            return OtherStuffSerializer().data
    
        @swagger_serializer_method(serializer_or_field=OtherStuffSerializer(many=True))
        def get_many_other_stuff(self, obj):
            return OtherStuffSerializer().data
    

    Note that the serializer_or_field parameter can accept either a subclass or an instance of serializers.Field.

  2. For simple cases where the method is returning one of the supported types, Python 3 type hinting of the serializer method return value can be used. e.g.:

    class SomeSerializer(serializers.Serializer):
        some_number = serializers.SerializerMethodField()
    
        def get_some_number(self, obj) -> float:
            return 1.0
    

    When return type hinting is not supported, the equivalent serializers.Field subclass can be used with swagger_serializer_method:

    class SomeSerializer(serializers.Serializer):
        some_number = serializers.SerializerMethodField()
    
        @swagger_serializer_method(serializer_or_field=serializers.FloatField)
        def get_some_number(self, obj):
            return 1.0
    

Serializer Meta nested class

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

class WhateverSerializer(Serializer):
   ...

   class Meta:
      ... options here ...

The available options are:

  • 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. If two serializers have the same ref_name, both their usages will be replaced with a reference to the same definition. If this option is not specified, all serializers have an implicit name derived from their class name, minus any Serializer suffix (e.g. UserSerializer -> User, SerializerWithSuffix -> SerializerWithSuffix)
  • swagger_schema_fields - a dictionary mapping Schema field names to values. These attributes will be set on the Schema object generated from the Serializer. Field names must be python values, which are converted to Swagger Schema attribute names according to make_swagger_name(). Attribute names and values must conform to the OpenAPI 2.0 specification.

Suppose you wanted to model an email using a JSONField to store the subject and body for performance reasons:

from django.contrib.postgres.fields import JSONField

class Email(models.Model):
    # Store data as JSON, but the data should be made up of
    # an object that has two properties, "subject" and "body"
    # Example:
    # {
    #   "subject": "My Title",
    #   "body": "The body of the message.",
    # }
    message = JSONField()

To instruct drf-yasg to output an OpenAPI schema that matches this, create a custom JSONField:

class EmailMessageField(serializers.JSONField):
    class Meta:
        swagger_schema_fields = {
            "type": openapi.TYPE_OBJECT,
            "title": "Email",
            "properties": {
                "subject": openapi.Schema(
                    title="Email subject",
                    type=openapi.TYPE_STRING,
                ),
                "body": openapi.Schema(
                    title="Email body",
                    type=openapi.TYPE_STRING,
                ),
            },
            "required": ["subject", "body"],
         }

class EmailSerializer(ModelSerializer):
    class Meta:
        model = Email
        fields = "__all__"

    message = EmailMessageField()

Warning

Overriding a default Field generated by a ModelSerializer will also override automatically generated validators for that Field. To add Serializer validation back in manually, see the relevant DRF Validators and DRF Fields documentation.

One example way to do this is to set the default_validators attribute on a field.

class EmailMessageField(serializers.JSONField):
   default_validators = [my_custom_email_validator]
   ...

Subclassing and extending

SwaggerAutoSchema

For more advanced control you can subclass 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 @swagger_auto_schema decorator described above, by setting it as a class-level attribute named swagger_schema on the view class, or globally via 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 Operation objects (e.g. overall document structure, vendor extensions in metadata) you can also subclass 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 generator_class of a SchemaView using get_schema_view().

Inspector classes

For customizing behavior related to specific field, serializer, filter or paginator classes you can implement the FieldInspector, SerializerInspector, FilterInspector, PaginatorInspector classes and use them with @swagger_auto_schema or one of the related settings.

A 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,)
   filterset_fields = ('title',)
   ...

A second example, of a FieldInspector that removes the title attribute from all generated Schema objects:

from drf_yasg.inspectors import FieldInspector

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 - Schema objects are sometimes output by reference (SchemaRef); in fact, that is how named models are implemented in OpenAPI:

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

This is implemented by only generating one 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.

IMPORTANT: nested fields on ModelSerializers that are generated from model ForeignKeys will always be output by value. If you want the by-reference behavior you have to explicitly set the serializer class of nested fields instead of letting ModelSerializer generate one automatically; for example:

class OneSerializer(serializers.ModelSerializer):
   class Meta:
      model = SomeModel
      fields = ('id',)


class AnotherSerializer(serializers.ModelSerializer):
   child = OneSerializer()

   class Meta:
      model = SomeParentModel
      fields = ('id', 'child')

Another caveat that stems from this is that any serializer named “NestedSerializer” will be forced inline unless it has a ref_name set explicitly.