How to show query parameter options in Django REST Framework – Swagger

Question:

This has been bugging me for a while now.

My ultimate goal is to show query parameter options inside SwaggerUI and give a form input for each query parameter. Similar to how it is displayed when providing a serializer for POST.

I am using a viewset which inherits from GenericViewSet and I have tried the following:

  • provide filter_fields attribute
  • provide and set filter_backends attribute to (filters.DjangoFilterBackend,)
  • provide filter_class defined inside my module.
  • Override options method to provide [actions][GET] information

Here’s a small catch, I am not using any models so I don’t think DjangoFilterBackend will really help me. I am using DjangoRESTFramework to talk to an outside API, and I am simply getting JSON result back, and passing it through to the frontend layer.

Here is a small modified snippet of my code to better explain my problem:

views.py

class SomeViewSet(GenericViewSet):
    # Note that I have all of these defined, but I have tried various combinations
    filter_fields = ('query_option_1', 'query_option_2',)
    filter_backeds = (filters.DjangoFilterBackend,)
    filter_class = SomeFilter
    query_metadata = some_dict

    # This works when request is OPTIONS
    def options(self, request, *args, **kwargs):
        if self.metadata_class is None:
            return self.http_method_not_allowed(request, *args, **kwargs)
        data = self.metadata_class().determine_metadata(request, self)
        data['actions']['GET'] = self.query_metadata
        return Response(data, status=status.HTTP_200_OK)

filters.py

class SomeFilter(FilterSet):
    strict = True
    query_option_1 = django_filters.NumberFilter(name='query_option_1')
    query_option_2 = django_filters.NumberFilter(name='query_option_2')

    class Meta:
        fields = ['query_option_1', 'query_option_2']

Thank you for looking, and thanks in advance for responding.

Asked By: dajee

||

Answers:

Okay, for those who stumble upon this question, I have figured it out. It is rather silly, and I feel a little stupid for not knowing, but in my defense, it was not clearly documented. The information was not found in DRF documentation, or inside Django REST Swagger repository. Instead it was found under django-rest-framework-docs, which is what Django REST Swagger is built off of.

To specify your query parameter to show up in your SwaggerUI as a form field, you simply comment like so:

def list(self):
    """
    param1 -- A first parameter
    param2 -- A second parameter
    """ 
    ...

And swagger will parse your comments and will put a form input for param1 and param2. What follows -- are the descriptions for the parameters.

Answered By: dajee

I found the rest framework swagger docs.
so we can write the parameter type(interger, char), response, etc.

the tripple --- is necessary.

@api_view(["POST"])
def foo_view(request):
    """
    Your docs
    ---
    # YAML (must be separated by `---`)

    type:
      name:
        required: true
        type: string
      url:
        required: false
        type: url
      created_at:
        required: true
        type: string
        format: date-time

    serializer: .serializers.FooSerializer
    omit_serializer: false

    parameters_strategy: merge
    omit_parameters:
        - path
    parameters:
        - name: name
          description: Foobar long description goes here
          required: true
          type: string
          paramType: form
        - name: other_foo
          paramType: query
        - name: other_bar
          paramType: query
        - name: avatar
          type: file

    responseMessages:
        - code: 401
          message: Not authenticated
    """

How about the situation we use the mixins class such as ModelViewSets.
Do we need to define the list function just to add the docs?
— No

We can do like this:

class ArticleViewSet(viewsets.ModelViewSet):

    """
    Articles.
    ---
    list:    #<--- here!!
        parameters:
            - name: name
              description: article title
    get_price:
        omit_serializer: true

    """

    @list_route(methods=['get'])
    def get_price(self, request):
        pass
Answered By: soooooot

New swagger

from rest_framework.filters import BaseFilterBackend
import coreapi

class SimpleFilterBackend(BaseFilterBackend):
    def get_schema_fields(self, view):
        return [coreapi.Field(
            name='query',
            location='query',
            required=False,
            type='string'
        )]

class MyViewSet(viewsets.ViewSet):
    filter_backends = (SimpleFilterBackend,)

    def list(self, request, *args, **kwargs):
        # print(request.GET.get('query'))  # Use the query param in your view
        return Response({'hello': 'world'}, status.HTTP_200_OK)
Answered By: vadimchin

Disclaimer: I am using django_filters, so results may vary. django_filters uses the param filter_fields in the DRF ViewSet, which may be different than not using django_filters.

I took inspiration from this thread and overrode the get_schema_fields() method in the filtering backend in the following way.

settings.py

REST_FRAMEWORK = {
    ...
    'DEFAULT_FILTER_BACKENDS': ('location.of.custom_backend.CustomDjangoFilterBackend')
    ...
}

custom_backend.py

import coreapi
import coreschema
from django_filters.rest_framework import DjangoFilterBackend


class CustomDjangoFilterBackend(DjangoFilterBackend):
    """
    Overrides get_schema_fields() to show filter_fields in Swagger.
    """

    def get_schema_fields(self, view):
        assert (
            coreapi is not None
        ), "coreapi must be installed to use `get_schema_fields()`"
        assert (
            coreschema is not None
        ), "coreschema must be installed to use `get_schema_fields()`"

        # append filter fields to existing fields
        fields = super().get_schema_fields(view)
        if hasattr(view, "filter_fields"):
            fields += view.filter_fields

        return [
            coreapi.Field(
                name=field,
                location='query',
                required=False,
                type='string',
            ) for field in fields
        ]
Answered By: Sean Chon

Elaborating on the answers above from @vadimchin – here is a working example.

# requirements.txt

djangorestframework==3.9.3
django-rest-swagger==2.2.0
django==2.2.1
coreapi==2.3.3

I am using Viewsets in my application. I had to implement filter_queryset(self, request, queryset, view) as suggested by @jarussi.

# models.py

from django.db import models 

class Recording(models.Model):
    _id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=511)
# serializers.py

from models import Recording
from rest_framework import serializers

class RecordingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Recording
        fields = '__all__'
# views.py

from rest_framework import viewsets
from filters import NameFilterBackend
from serializers import RecordingSerializer

class RecordingViewSet(viewsets.ModelViewSet):
    serializer_class = RecordingSerializer
    queryset = Recording.objects.all()
    filter_backends = (NameFilterBackend,)
# filters.py 

from rest_framework.filters import BaseFilterBackend
import coreapi

class NameFilterBackend(BaseFilterBackend):
    def get_schema_fields(self, view):
        return [coreapi.Field(
            name='name',
            location='query',
            required=False,
            type='string',
            description='name of recording'
        )]

    def filter_queryset(self, request, queryset, view):
        try:
            n = request.query_params['name']
            queryset = queryset.filter(name=n)
        except KeyError:
            # no query parameters
            pass
        return queryset
Answered By: user11471297

Please refer this github issue which solves the problem.

Answered By: Devaroop

Working with openapi (and not coreapi), the “simplest” way I found is from this core dev comment:

from rest_framework.schemas.openapi import AutoSchema


class CustomSchema(AutoSchema):
    def get_operation(self, path, method):
        op = super().get_operation(path, method)
        op['parameters'].append({
            "name": "foo",
            "in": "query",
            "required": True,
            "description": "What foo does...",
            'schema': {'type': 'string'}
        })
        return op


class MyViewSet(ModelViewSet):
    schema = CustomSchema()

    def get_queryset(self):
        foo = self.request.query_params.get("foo")
        if foo:
            self.queryset = self.queryset.filter(foo=foo)
        return self.queryset
Answered By: NiKo

If the query parameters are used in a filter backend, adding a get_schema_operation_parameters method is the simplest solution:

class SimpleFilterBackend(BaseFilterBackend):
    def filter_queryset(self, request, queryset, view):
        foo = request.query_params.get("foo")
        if foo:
            queryset = queryset.filter(foo=foo)
        return queryset

    def get_schema_operation_parameters(self, view):
        return [{
            "name": "foo",
            "in": "query",
            "required": True,
            "description": "What foo does...",
            "schema": {"type": "string"}
        }]

class MyViewSet(ModelViewSet):
    filter_backends = [SimpleFilterBackend]
Answered By: saikarsis

For anyone stumbling upon this later, I pieced together a few solutions and came up with this:

This is using the new openapi instead of coreapi.

I subclassed AutoSchema to accept a dictionary keyed on action name and mapping to the OpenApi Operation Parameter object spec. These params get appended to the operation.

from rest_framework.schemas.openapi import AutoSchema

class ParameterSchema(AutoSchema):
    def __init__(self, **kwargs):
        self.parameters = kwargs.pop("parameters")
        super().__init__(**kwargs)

    def get_operation(self, path, method):
        op = super().get_operation(path, method)
        method_name = getattr(self.view, "action", method.lower())
        action_parameters = self.parameters.get(method_name, [])
        for param in action_parameters:
            op["parameters"].append(param)
        return op


class MyViewSet(viewsets.ModelViewSet):
    schema = ParameterSchema(
        parameters={
            "list": [
                {
                    "name": "my-param",
                    "in": "query",
                    "required": False,
                    "description": "Description on the param",
                    "schema": {"type": "boolean", "default": False},
                }
            ]
        }
    )
Answered By: t.money.d