User guide

Operations

The API documentation will be extracted from the view class and methos docstrings. You can add documentation the following way:

class MyApiViewSet(viewsets.ModelViewSet):
    """
    General description. It will be used for `tags` and operations (actions) that doesn't
    provide a specific description.


    list: this is the description of the list action

    retrieve: this is the description of the retrieve action

    mycustomaction: this is `mycustomaction` description and is the same regardless of http method.

    another_action$get: this is `another_action` description just for the `get` http method.
    """

    ...

If an action has a custom implementation, its description is taken from the method docstring. The method docstring take precedence over the class docstring.

Headers

You can document http headers both for requests and responses.

class MyApiViewSet(viewsets.ModelViewSet):
    schema_request_headers = {
        'list': { # you could also use a method (lowercase) or '' to append to each request.
            'X-My-Request-Header': {
                'type': 'string',
                'description': 'This is a request header for the list action',
            }
        }
    }
    schema_response_headers = {
        'post,put': { # You could provide a comma-separeted list of methods and/or actions.
            'X-My-Response-Header': {
                'type': 'string',
                'description': 'This is a response header for all kind of operations'.
            }
        }
    }

If declared here, it takes precedence over the ones that are declared in the DRF_CONNECT_DOCS setting for the same action or method.

Filters

django-rql offers support for schema generation from release 3 onward.

Please refers to https://git.int.zone/projects/SWFT/repos/django-rql/browse for more information.

Path parameters

Path parameters are extracted from the path. If a parameter name match a model field its description is taken from the model field help_text attribute.

Manual parameters

Querystring parameters can be described manually using the schema_manual_parameters attribute of a view.

class MyView(views.APIView):
    schema_manual_parameters = [
        {
            'name': 'my_param',
            'in': 'query',
            'required': True,
            'schema': {
                'type': 'string',
            },
        },
    ]

Serializers

To add a description to the Model handled by a serializer, simply add the class docstring to the serializer as for example:

class AssetDetailSerializer(serializers.ModelSerializer):
    """This is the description of the *AssetDetail* model"""

SerializerMethodField (or the to_representation serializer method)

In order to document the JSON subobject returned by the SerializerMethodField’s method you can use the @schema_method decorator.

For example if the method returns a scalar value (string, integer, decimal, etc) you can pass the corresponding serializers field:

...

@schema_method(field=serializers.EmailField)
def get_email(self, obj):
    return 'test@email.com'
...

or if the method returns an object using a Serializer, pass the serializer (or a dotted module path) as an argument of the decorator:

...

@schema_method(serializer=AssetReferenceSerializer)
def get_asset(self, obj):
    asset_serializer = AssetReferenceSerializer(obj.asset, read_only=True)
    setattr(asset_serializer, '__tiers', self.__tiers)
    return asset_serializer.data

...

If the method returns an array of objects you can specify many=True:

...

@schema_method(serializer=AssetReferenceSerializer, many=True)
def get_assets(self, obj):
    asset_serializer = AssetReferenceSerializer(obj.assets, read_only=True, many=True)
    return asset_serializer.data

...

finally, if the returned object is more complex and can’t be described using a serializer you can pass the schema for this object manually:

...

@schema_method(schema={
    'type': 'object',
    'properties': {
        'created': {
            'type': 'object',
            'properties': {
                'at': {
                    'type': 'string',
                    'format': 'date-time',
                },
            },
        },
        'updated': {
            'type': 'object',
            'properties': {
                'at': {
                    'type': 'string',
                    'format': 'date-time',
                },
            },
        },
    },
})
def get_events(self, obj):
    return {
        'created': {'at': obj.created_at.isoformat() if obj.created_at else None},
        'updated': {'at': obj.updated_at.isoformat() if obj.updated_at else None},
    }

...

To describe a schema with the minimum human effort, it also accept serializers and fields:

...

@schema_method(schema={
    'type': 'object',
    'properties': {
        'stats': {
            'type': 'object',
            'properties': {
                'vendor': {
                    'type': 'object',
                    'properties': {
                        'last_request': BillingRequestSummarySerializer,
                        'count': serializers.IntegerField,
                    },
                },
                'provider': {
                    'type': 'object',
                    'properties': {
                        'last_request': BillingRequestSummarySerializer,
                        'count': serializers.IntegerField,
                    },
                },
            },
        },
    },
})
def get_billing(self, obj):
    billingstat_serializer = BillingStatSerializer(many=True)
    billing = billingstat_serializer.to_representation(obj.item.billing_request_stats.all())
    return {'stats': dict(billing)} if billing else {}

...

You can also use the @schema_method decorator to decorate the to_representation method for serializers, rest_framework.serializers.RelatedField and rest_framework.serializers.ReadOnlyField:

...

@schema_method(serializer=BillingRequestGetSerializer())
def to_representation(self, instance):
    return BillingRequestGetSerializer(instance).data

...

The schema argument of the @schema_method decorator accept also a callable with no arguments or an instance method of the same serializer. For example:

class MySerializer(serializers.Serializer):
    events = serializers.SerializerMethodField()

    def get_schema_for_events(self):
        return {
            'type': 'object',
            'properties': {
                ...
            }
        }

    @schema_method(schema=get_schema_for_events)
    def get_events(self, obj):
        pass

JSONField

If you want to describe schema for a JSONField, you can subclass it and provide a schema attribute for example:

class MyCoolJSONField(serializers.JSONField):
    schema = {
        'type': 'object',
        'properties': {
            'name': {
                'type': 'string'
            },
            'birth_date': {
                'type': 'string',
                'format': 'date',
            },
        },
    }

Manual responses

You can document responses for other http status codes than 2xx this way:

class MyApiViewSet(viewsets.ModelViewSet):
    schema_responses = {
        'list': { # Like headers, you can use action or method (both lowercase) or ''.
            '400': {
                'content': {
                    'application/json': {
                        'schema': {
                            'type': 'object',
                            'properties': {
                                'error_code': {
                                    'type': 'string',
                                },
                                'errors': {
                                    'type': 'array',
                                    'items': {
                                        'type': 'string',
                                    },
                                },
                            },
                        },
                    },
                },
                'description': '',
            },
        }
    }

If declared here, they take precedence over the ones that are declared in the DRF_CONNECT_DOCS setting for the same action or method.

Groups operations with tags

By default, drf-connect-docs groups view’s operations within a tag. The tag name will be the name of the view (without View, ViewSet etc.) and the description will be the general description from the View docstring.

If you have declared tags in the DRF_CONNECT_DOCS setting, you can associate views to a tag doing the following:

class MyViewSet(ViewSet):
    schema_tag = 'name_of_the_tag'

Schema Examples

Sometimes it is useful to provide request/response examples of your endpoints. To provide examples you can use schema_examples property of the View class

To provide example for overall responses:

class MyViewSet(ViewSet):
    serializer_class = DefaultSerializer
    schema_examples = {
        'response': 'openapi/example_response.json',
    }

The library now will include JSON example as a default response example and add it to the generated documentation file.

Also you can provide request/response examples more precisely, using overrides:

class MyViewSet(ViewSet):
    serializer_class = DefaultSerializer
    schema_examples = {
        'response': 'openapi/example_response.json',
        'overrides': {
            'create': {  # view action
                'request': 'openapi/create_request_example.json',
                'response': 'openapi/create_response_example.json',
            },
        },
    }

JSON examples should contain summary, description, value fields

{
    "summary": "Entity object.",
    "description": "",
    "value": {
        "id": "ID-123-123-123-123",
        "status": "successsed"
    }
}

Where

  • summary - short title of the response

  • description - short description of the response

  • value - JSON example of the response

For Request JSON examples you can provide several examples in one file, using JSON keys

{
    "successed": {
        "summary": "Create successed Entity",
        "value": {
            "status": "successed"
        }
    },
    "failed": {
        "summary": "Create failed Entity",
        "value": {
            "status": "failed"
        }
    }
}

For this example library will generate several examples for the same request/response

Customization

Per-view customization

Please refer to https://www.django-rest-framework.org/api-guide/schemas/#per-view-customization if you want to completely disable schema generation for a view/viewset or provide a ConnectAutoSchema subclass to a view with customization.

Different request/response serializer

If an action uses a serializer for the request and another for the response you can describe this situation the following way:

class MyViewSet(ViewSet):
    serializer_class = DefaultSerializer

    def get_request_serializer_class(self):
        if self.action == 'create':
            return CreateRequestSerializer

    def get_response_serializer_class(self):
        if self.action == 'create':
            return CreateResponseSerializer

In this example, if the action is create the CreateRequestSerializer and CreateResponseSerializer` for request and response bodies respectively.

Customize schema for specific action/method

If you want a completely custom schema for request and/or response:

class MyViewSet(ViewSet):

    def get_request_schema(self):
        if self.action == 'create':
            return {
                'content': {
                    'application/json': {
                        'schema': {
                            'type': 'object',
                            'properties': {
                                'my_prop': {'type': 'string'},
                            },
                        },
                    },
                },
            }

    def get_response_schema(self):
        if self.action == 'create':
            return {
                '201': {
                    'content': {
                        'application/json': {
                            'schema': {
                                'type': 'object',
                                'properties': {
                                    'my_prop': {'type': 'string'},
                                },
                            },
                        },
                    },
                },
            }

Note that if the returned value from get_request_schema and get_response_schema is None, drf-connect-docs will continue with the serializer inspection.

Customize tags

You can customize the tag name and description for a per-view basis. To do so you can add the attribute schema_tag to the view:

class MyTaggedViewSet(ViewSet):

    schema_tag = (
        'tag_name',
        'tag_description_with_markdown_support',
        'optional_external_doc_url',
    )

If a tag with the same name has already been declared in the DRF_CONNECT_DOCS setting, it takes precedence over this one.

Customize models names

drf-connect-docs determine the name of a model from the name of the serializer. For example if we have a serializers named MyModelSerializer the model name will be MyModel.

To customize the name of a model (represented by a DRF serializer) you have to add the schema_name attribute to the serializer Meta.

For example if we have the following:

class AssetListSerializer(AssetBaseSerializer):
    items = ItemAssetReferenceSerializer(source='assetitem_set', many=True, read_only=True)
    events = serializers.SerializerMethodField()

    class Meta:
        model = Asset
        schema_name = 'Assets'

the model will be named Assets instead of AssetList.

Generate the OpenAPI 3 schema file

The schema could be generated on-demand using the generate_schema django command:

$ python manage.py generate_schema > myschema.yml

In some circumstances the lack of authentication information inside the request object can cause an exception. In this case it’s possible to pass an api key through the Authorization header with the option:

$ python manage.py generate_schema --api-key "ApiKey SU-XXXX-XXXX:xxxxxxxxxxxx" > myschema.yml

It could also be generated automatically on application bootstrap with these settings:

DRF_CONNECT_DOCS = {
    'OUTPUT_SCHEMA_ON_START': True,
    'OUTPUT_SCHEMA_FILE': '/path/to/schema/folder/specs.yml',
}

Serve the schema

If you want that your django service expose a view to serve the schema you have to include the urlpatterns from the cnctdocs app in your URLConf:

from django.urls import include, path

from cnctdocs import urls as schema_view_urls

urlpatterns = [
    ...
    path('/my/cool/path/', include(schema_view_urls)),
    ...
]

And you must set the path where the schema file is located:

DRF_CONNECT_DOCS = {
    'OUTPUT_SCHEMA_FILE': '/path/to/schema/folder/specs.yml',
}

Finally you must run collectstatic:

$ ./manage.py collectstatic

This results in a view available at /my/cool/path/specification. If Django is in DEBUG mode, /my/cool/path/specification-ui will serve a view with an embedded swagger-ui component to make the life easier to programmers since they can look at how documentation is rendered and find possible problems quickly.

You can customize the last fragment of the path for example:

DRF_CONNECT_DOCS = {
    'SCHEMA_VIEW_URL_PATH': 'specs/openapi',
}

So the resulting url will be /my/cool/path/specs/openapi.

By default, the specification view inherits authentication and permission classes respectively from the DRF’s DEFAULT_AUTHENTICATION_CLASSES and DEFAULT_PERMISSION_CLASSES settings.

If you want to customize authentication or permissions for the view you have to set:

DRF_CONNECT_DOCS = {
    'SCHEMA_VIEW_AUTHENTICATION_CLASSES': 'my.custom.Authentication',
    'SCHEMA_VIEW_PERMISSION_CLASSES': 'my.custom.Permission',
}

or if you want to configure more than one authentication or permission class you can use a tuple or list:

DRF_CONNECT_DOCS = {
    'SCHEMA_VIEW_AUTHENTICATION_CLASSES': ('my.custom.Authentication1', 'my.custom.Authentication1'),
    'SCHEMA_VIEW_PERMISSION_CLASSES': ('my.custom.Permission1', 'my.custom.Permission2'),
}

Merge schemas

The merge_schemas merge several slaves schemas file into a master. The general information, servers and authentication sections of slaves is ignored. If some component or tag is present both in the master schema and one of the slaves schemas, the merge fails.

The merge_schemas command works both with local files or files served by a webserver.

$ python manage.py merge_schemas \
    --master https://example.com/devportal.yml \
    https://example.com/subscriptions.yml \
    https://example.com/usage.yml > merged.yml