Django Rest Framework OpenAPI 3 support
OpenAPI 3 support in Django Rest Framework is still a work in progress. Things are moving quickly so there's not a lot of up to date info about this topic. It's not clear which features of OpenAPI 3 spec are supported in DRF and researching this info on your own means wasting a lot of time.
In this article I'll go over the sections of the OpenAPI spec and talk about its support in DRF. If you don't know how to generate an OpenAPI 3 spec in DRF you can read about it here.
Since things change quickly I'll try to keep this post up to date (DRF version tested in this post: 3.10.2).
Overview
Here's a bird's-eye view of an OpenAPI spec:
- info (general API info like title, license, etc)
- servers (basically a base url for your API service)
- paths (this is your actual application)
- path (url to a DRF view)
- operation (
get
,post
etc)- parameters (url/query, headers and cookies parameters)
- request
- media types (e.g
application/json
)- body schema
- media types (e.g
- response
- status code (e.g.
200
or500
)- media types
- body schema
- media types
- status code (e.g.
- operation (
- path (url to a DRF view)
- components (chunks of schema that can be reused in this spec)
The ideal scenario is where DRF generates an OpenAPI schema by inferring as much info from the application code as possible. It is not difficult to populate the info
and servers
parts, so we are not really interested in them. Ideally, DRF should generate the paths
section of the spec, as this is where the actual application is described. components
section allows to keep schema readable and short by defining and reusing certain spec parts, however DRF doesn't use it at all. If DRF doesn't generate something automatically, it is still possible to customize the process by overriding a SchemaGenerator
or AutoSchema
(view inspector).
Paths and Operations
In DRF a path is an endpoint URL and an operation is an actual view (for CBVs it is a method that handles an HTTP verb, e.g. GET
). Paths and operations spec is generated automatically in DRF. It can infer supported HTTP verbs for both FBVs and CBVs.
# views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
class CBView(APIView):
def get(self, request, *args, **kwargs):
return Response()
def post(self, request, *args, **kwargs):
return Response()
@api_view(['GET', 'POST'])
def fb_view(request):
return Response()
# urls.py
urlpatterns = [
path('api/cb_view/', views.CBView.as_view()),
path('api/fb_view/', views.fb_view),
]
The above code would result in the following OpenAPI spec (I only included a paths
section since this is the one we're mostly interested in):
paths:
/api/cb_view/:
get:
operationId: ListCBs
parameters: []
responses:
'200':
content:
application/json:
schema: {}
post:
operationId: CreateCB
parameters: []
responses:
'200':
content:
application/json:
schema: {}
/api/fb_view/:
get:
operationId: Listfb_views
parameters: []
responses:
'200':
content:
application/json:
schema: {}
post:
operationId: Createfb_view
parameters: []
responses:
'200':
content:
application/json:
schema: {}
Besides weird operationId
values, all urls and views are handled correctly. I also tested ViewSets and Generic Views and they all produced correct paths and operations spec. Finally, DRF metadata spec generation is not supported out of the box, however this is not an important feature.
Parameters
There are 4 kinds of parameters in OA3 spec: path, query, header and cookie. In DRF parameters are automatically inferred from urls, builtin filters and pagination backends.
URL parameters
# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
class Record(APIView):
def get(self, request, *args, **kwargs):
return Response()
# urls.py
urlpatterns = [
path('api/records/<int:pk>/<uuid:uuid>', views.Record.as_view()),
]
paths:
/api/records/{id}/{uuid}:
get:
operationId: RetrieveRecord
parameters:
- name: uuid
in: path
required: true
description: ''
schema:
type: string
- name: id
in: path
required: true
description: ''
schema:
type: string # should have been an integer
responses:
'200':
content:
application/json:
schema: {}
As you can see, DRF infers multiple parameters in URLs, however it doesn't support automatic path converters to schema type
mapping, so all path parameters are treated as a string
. So all the essential functionality is there, which is great! Unfortunately, you can't customize various parameter attributes (e.g. required
, description
, etc) without overriding the whole view inspector class.
Builtin filters, pagination and custom parameters
Builtin DRF filters and pagination backends come with their own parameters. Fortunately, DRF includes them in a spec automatically. If your endpoint accepts custom parameters and you'd like to include them in a spec, you should define them via custom filters by overriding get_schema_operation_parameters
:
from rest_framework.views import APIView
from rest_framework.filters import BaseFilterBackend, OrderingFilter
class CustomFilter(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset
def get_schema_operation_parameters(self, view):
return [
{
'name': 'q',
'required': False,
'in': 'query',
'schema': {
'type': 'string',
},
},
{
'name': 'X-Param',
'required': False,
'in': 'header',
'schema': {
'type': 'string',
},
},
]
class BookList(APIView):
pagination_class = PageNumberPagination
filter_backends = [OrderingFilter, CustomFilter]
def get(self, request, *args, **kwargs):
pass
paths:
/api/books/:
get:
operationId: ListBooks
parameters:
# builtin parameters
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
# custom parameters
- name: q
required: false
in: query
schema:
type: string
- name: X-Param
required: false
in: header
schema:
type: string
responses:
'200':
content:
application/json:
schema: {}
Specifying parameters via custom filters each time might be an overkill but it is probably a good code pattern to follow anyway. Here's what DRF doesn't support: inferring parameters from builtin authentication classes, versioning, format suffixes are somewhat broken (don't work with ViewSets, produce duplicate operationId
, etc).
Request and Response body
OA3 allows to specify body media types supported by an endpoint. DRF has a concept of parsers and renderers, where the former handles the request body and the latter – the response body. By default, each DRF view is configured to support 3 request parsers: JSONParser
, FormParser
and MultiPartParser
. Those parsers handle the following media types: application/json
, application/x-www-form-urlencoded
and multipart/form-data
. This means that you can submit the same contents using different request body formats and DRF would handle them (e.g. {"title": "Example"}
is the same as title=Example
).
When it comes to OA3 spec generation, DRF doesn't infer anything from the view's parsers or renderers to generate appropriate media types, it produces application/json
in every scenario. This is a problem and I've already opened a PR which addresses this.
There is no support for multiple response codes: at the moment every response spec is generated with a 200
status code and there is no way to specify other responses without overriding a view inspector. To customize any request or response attributes (e.g. description
, required
, response headers, etc), you'd need to override a view inspector too.
Schema
An actual body schema is produced by inferring the fields and its attributes from DRF serializer. As I said in the beginning, DRF doesn't utilize the components
section, that's why it doesn't put the generated schema in this section. This means the body spec is duplicated for request and response. Also, even though this is mostly a DRF limitation, you can't use distinct schemas (via oneOf
, anyOf
, etc) for request, response or both.
When it comes to actual mapping of serializer fields to OA3 schema fields, DRF does a pretty good job. The majority of keywords and attributes are supported correctly. Here's a sample model and a serializer where I tried to include as much various field and attribute combinations as I could to demo the functionality:
# models.py
from django.db import models
from django.core.validators import MinLengthValidator
class Author(models.Model):
first_name = models.CharField(max_length=200)
class Book(models.Model):
KINDS = [
('hc', 'Hardcover'),
('sc', 'Softcover')
]
title = models.CharField(max_length=200)
kind = models.CharField(max_length=2, choices=KINDS)
description = models.CharField(max_length=300, validators=[MinLengthValidator(10)])
pages = models.IntegerField(null=True)
status = models.BooleanField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# serializers.py
from rest_framework import serializers
from .models import Author, Book
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = '__all__'
class NestedSerializer(serializers.Serializer):
title = serializers.CharField()
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
nested_test = NestedSerializer()
char_test = serializers.CharField(default='value', required=False, help_text='Test field')
email_test = serializers.EmailField()
ip_test = serializers.IPAddressField('IPv4')
class Meta:
model = Book
fields = '__all__'
# views.py
from rest_framework import generics
class BookDetail(generics.UpdateAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
allowed_methods = ['put']
paths:
/api/books/{id}/:
put:
operationId: UpdateBook
parameters:
- name: id
in: path
required: true
description: A unique integer value identifying this book.
schema:
type: string
requestBody:
content:
application/json:
schema:
required:
- author
- nested_test
- email_test
- ip_test
- title
- kind
- description
- status
properties:
author:
required:
- first_name
properties:
id:
type: integer
readOnly: true
first_name:
type: string
maxLength: 200
type: object
nested_test:
required:
- title
properties:
title:
type: string
type: object
char_test:
type: string
default: value
description: Test field
email_test:
type: string
format: email
ip_test:
type: string
format: ipv4
title:
type: string
maxLength: 200
kind:
enum:
- hc
- sc
description:
type: string
maxLength: 300
minLength: 10
pages:
type: integer
nullable: true
status:
type: boolean
responses:
'200':
content:
application/json:
schema:
required:
- author
- nested_test
- email_test
- ip_test
- title
- kind
- description
- status
properties:
id:
type: integer
readOnly: true
author:
required:
- first_name
properties:
id:
type: integer
readOnly: true
first_name:
type: string
maxLength: 200
type: object
nested_test:
required:
- title
properties:
title:
type: string
type: object
char_test:
type: string
default: value
description: Test field
email_test:
type: string
format: email
ip_test:
type: string
format: ipv4
title:
type: string
maxLength: 200
kind:
enum:
- hc
- sc
description:
type: string
maxLength: 300
minLength: 10
pages:
type: integer
nullable: true
status:
type: boolean
As you can see DRF supports a lot of OA3 schema fields features. There are still some bugs or unsupported parts but it's not a big deal. Here's a rough list of what is not supported (this is what I've been able to find, it's not an exhaustive list):
ListField
min_length
&max_length
attributes don't map tominItems
&maxItem
schema attributes- ignores the attributes of a
child
's field
- Related fields
PrimaryKeyRelatedField
results intype: string
(should beinteger
)HyperlinkedRelatedField
,HyperlinkedIdentityField
could have includedformat: uri
.
DictField
: ignores the attributes of achild
's fieldFileField
: doesn't add aformat: binary
to a field's schemaread_only
fields are not included in request body,write_only
fields are not included in response body. Not sure about this one, but I think it should include the same set of fields in both request & response with relevant attributes
These are not big issues, so the field mapping support is still pretty good.
Unreleased but solved issues
In other words issues that are already fixed but not yet released. Here's a list of new commits since the latest release, most of them are about OpenAPI schema generation.
There is also a number of OpenAPI related PRs that are either completed but not merged or still in progress:
- Add support for pagination in OpenAPI response schemas
- Fixed incorrect min/max attributes for serializers.ListField
- OpenAPI Schema inconsistent operationId casing
Summary
As you can see the OpenAPI 3 support in DRF is still far from complete but the majority of the functionality is there. Improved API for spec customization would also be nice (e.g. overriding a method to customize a schema attribute rather than a whole view inspector). This is a work in progress, so hopefully the issues will be fixed within the next couple releases. For now, the best approach to work with a spec would be to generate a static version and then modifying the relevant parts manually.