drf-yasg/docs/custom_spec.rst

401 lines
19 KiB
ReStructuredText

.. |br| raw:: html
<br />
########################
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 <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
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
- ``{path}``: :class:`.PathItem` - each :class:`.PathItem` has multiple operations keyed by method
* ``{http_method}``: :class:`.Operation`
Each operation is thus uniquely identified by its ``(path, http_method)`` combination,
e.g. ``GET /articles/``, ``POST /articles/``, etc.
* ``parameters``: [:class:`.Parameter`] - and a list of path parameters
+ ``definitions``: named Models
A list of all the named models in the API in the form of a mapping
- ``{ModelName}``: :class:`.Schema`
* :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.
- there can also be **at most one** body parameter whose structure is represented by a
:class:`.Schema` or a reference to one (:class:`.SchemaRef`)
+ ``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.
- ``{status_code}``: :class:`.Response` - mapping of status code to response definition
+ ``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 |br| |
| | Can only nest a Schema if the parameter is ``in: body`` |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot describe file uploads |br| | Can describe file uploads via ``type`` = ``file``, |br| |
| - ``file`` is not permitted as a value for ``type`` | but only as part of a form :class:`.Operation` [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can be used in :class:`.Response`\ s | Cannot be used in :class:`.Response`\ s |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot be used in form :class:`.Operation`\ s [#formop]_ | Can be used in form :class:`.Operation`\ s [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can only describe request or response bodies | Can describe ``query``, ``form``, ``header`` or ``path`` |
| | parameters |
+----------------------------------------------------------+-----------------------------------------------------------+
.. [#formop] 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.
* :class:`.Paths` are generated by exploring the patterns registered in your default ``urlconf``, or the ``patterns``
and ``urlconf`` you specified when constructing :class:`.OpenAPISchemaGenerator`; only views inheriting from Django
Rest Framework's ``APIView`` are looked at, all other views are ignored
* ``path`` :class:`.Parameter`\ s are generated by looking in the URL pattern for any template parameters; attempts are
made to guess their type from the views ``queryset`` and ``lookup_field``, if applicable. You can override path
parameters via ``manual_parameters`` in :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
* ``query`` :class:`.Parameter`\ s - i.e. parameters specified in the URL as ``/path/?query1=value&query2=value`` -
are generated from your view's ``filter_backends`` and ``paginator``, if any are declared. Additional parameters can
be specified via the ``query_serializer`` and ``manual_parameters`` arguments of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`
* The request body is only generated for the HTTP ``POST``, ``PUT`` and ``PATCH`` methods, and is sourced from the
view's ``serializer_class``. You can also override the request body using the ``request_body`` argument of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
- if the view represents a form request (that is, all its parsers are of the ``multipart/form-data`` or
``application/x-www-form-urlencoded`` media types), the request body will be output as ``form``
:class:`.Parameter`\ s
- if it is not a form request, the request body will be output as a single ``body`` :class:`.Parameter` wrapped
around a :class:`.Schema`
* ``header`` :class:`.Parameter`\ s are supported by the OpenAPI specification but are never generated by this library;
you can still add them using ``manual_parameters``.
* :class:`.Responses` are generated as follows:
+ if ``responses`` is provided to :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` and contains at least
one success status code (i.e. any `2xx` status code), no automatic response is generated and the given response
is used as described in the :func:`@swagger_auto_schema documentation <.swagger_auto_schema>`
+ otherwise, an attempt is made to generate a default response:
- the success status code is assumed to be ``204` for ``DELETE`` requests, ``201`` for ``POST`` requests, and
``200`` for all other request methods
- if the view has a request body, the same ``Serializer`` or :class:`.Schema` as in the request body is used
in generating the :class:`.Response` schema; this is inline with the default ``GenericAPIView`` and
``GenericViewSet`` behavior
- if the view has no request body, its ``serializer_class`` is used to generate the :class:`.Response` schema
- if the view is a list view (as defined by :func:`.is_list_view`), the response schema is wrapped in an array
- if the view is also paginated, the response schema is then wrapped in the appropriate paging response structure
- the description of the response is left blank
* :class:`.Response` headers are supported by the OpenAPI specification but not currently supported by this library;
you can still add them manually by providing an `appropriately structured dictionary
<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#headersObject>`_
to the ``headers`` property of a :class:`.Response` object
* *descriptions* for :class:`.Operation`\ s, :class:`.Parameter`\ s and :class:`.Schema`\ s are picked up from
docstrings and ``help_text`` attributes in the same manner as the `default DRF SchemaGenerator
<http://www.django-rest-framework.org/api-guide/schemas/#schemas-as-documentation>`_
.. _custom-spec-swagger-auto-schema:
**************************************
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``,
.. code:: python
@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_view``\ s, 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:
.. code:: python
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:
.. code:: python
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. |br|
Additionally, ``@list_route``\ s or ``@detail_route``\ s 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:
.. code:: python
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``:
.. code:: python
@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:
.. code:: python
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.:
.. code:: python
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:
.. code:: python
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`.
.. _custom-spec-inspectors:
---------------------
``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:
.. code:: python
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:
.. code:: python
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 ``FieldInspector``\ s 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.