Migrating from Tastypie to Django REST Framework
All notes in this series:
Here are some tips from my experience of migrating an existing Django application from using Tastypie for its REST API to instead using Django REST Framework.
These notes assume the following versions:
Separate v1 and v2 APIs §
Tastypie hosted its API at /api/v1/
. To ensure a smooth transition, we configured Django REST Framework to be exposed at /api/v2/
. The plan was to build the new v2 API in a manner that is backwards-compatible with the old v1 API. Clients to the REST API could then switch between either using the v1 or v2 API with a simple configuration change.
Here is how to run both Tastypie at /api/v1/
and DRF at /api/v2/
:
Filtering §
Out of the box, Django REST Framework does not support the same filtering (and ordering) that Tastypie does. Use django-filter to add support for this.
Then create a filters.py
and place a filter in there for each view set:
Then in your views.py
update the view set to use that filter:
Filtering expressions (lt
, lte
, gt
, gte
, etc.) §
Note however that by default this only adds exact matching. To add support for other filtering expressions, you must add an additional filter which uses the lookup_expr
keyword argument for each additional expression you wish to support. It feels like there should be a simpler way to do this, but this is the only way I’ve found.
For example, to build upon our previous FooBarFilter
and add support for querying timestamps less than (or equal to) or greater than (or equal to):
Filtering with choice fields §
Some extra work is required if you wish to have a choices field whereby the integer values are used internally (in the database) but the string values are used externally (for REST API parameters). Say you have the following Django model:
You might then expect to be able to use django-filter to allow filtering like so:
That will work, but the REST API parameter will have to be the integer value rather than a string, e.g.:
We want to use the string, e.g.:
The simplest way I found to do this is by overriding the filter’s filter
method to map the string to the integer before the filtering takes place.
First, we must reverse the mapping of the CHOICES
. This is necessary because the left-hand item in the key, value pairs is expected by django-filter in the URL (e.g. ?status=xxx
), and we want it to expect the string rather than the integer.
Next we subclass filters.ChoiceFilter
to swap the string for the integer before the filtering takes place. We can use the REVERSE_CHOICES
as a dictionary for this, since it contains a mapping of strings to integers.
Finally, we simply update the WidgetFilter
to use our MappingChoiceFilter
. Note that because fitlers.ChoiceFilter
(which MappingChoiceFitler
inherits from) validates that the incoming string is a key from the REVERSE_CHOICES
dictionary, we do not need to expect any KeyError
when we do the swapping via the dictionary lookup in MappingChoiceFilter.filter
, since the key is guaranteed to exist.
At last this allows us to query the choice through the API with the string, whilst using the integer internally:
Ordering §
Tastypie style ordering is built in to Django REST Framework, but requires a small configuration change to enable and another change to be compatible.
First you must add "rest_framework.filters.OrderingFilter"
to the DEFAULT_FILTER_BACKENDS
to enable filtering by default. (Alternatively you can enable it on a case-by-case basis for each view).
Note however that while Tastypie used the order_by
parameter, Django REST Framework uses ordering
. To override this, use the ORDERING_PARAM
setting:
Pagination §
By default, Tastypie used limit and offset ordering, while Django REST Framework uses page ordering.
Fortunately it is trivial to switch Django REST Framework to use limit and offset ordering with the built in LimitOffsetPagination
paginator.
Pagination Format §
Further, the response returned by Django REST Framework’s paginator is slightly different to the default response given by Tastypie. While Tastypie returns something like:
Django REST Framework returns something like:
Fortunately it is easy to configure Django REST Framework to use a custom paginator, specifically if you override the get_paginated_response
class you can configure the JSON exactly as you like.
Hydrate and dehydrate §
In Tastypie, the hydrate methods are used to add extra functionality when converting incoming JSON to a Python object, and likewise the dehydrate methods are used to add extra functionality when converting an outgoing Python object to JSON. This is extremely useful for cases such as adding in extra fields to the JSON response, renaming fields, or converting values (e.g. to handle enumerations).
The equivalent of this in Django REST Framework is to override the to_representation
(aka dehydrate) and to_internal_value
(aka hydrate) methods.
For example:
Validation §
If you just want to validate a field or group of fields in the serializer, then use the validate_<field>
or validate
methods respectively.
However, if you want to validate and convert modify it, then you must perform the validation in the to_internal_value
. This is necessary because validate
(or validate_<field>
) is run after to_internal_value
. (Also, the validation order is not officially documented, so it is safer not to tie ourselves in to relying upon the current execution order).
Note that there is a subtle difference in how you should raise a validation error in validate_<field>
compared to validate
and to_internal_value
.
In validate_<field>
you can simply raise the exception with a string:
However in to_internal_value
(or validate
) you must pass in a dictionary which maps the fields to the errors:
Both of these would then return an error such as the following:
This subtle difference is mentioned in the DRF serializers documentation (emphasis mine):
If any of the validation fails, then the method should raise a
serializers.ValidationError(errors)
. The errors argument should be a dictionary mapping field names (orsettings.NON_FIELD_ERRORS_KEY
) to a list of error messages. If you don’t need to alter deserialization behavior and instead want to provide object-level validation, it’s recommended that you instead override the.validate()
method.
If you fail to pass in a dictionary when necessary, you will get a traceback like the following:
Supporting PATCH
§
All viewsets.ModelViewSet
view sets support PATCH
by default under Django REST Framework. Note however that if you have overridden to_internal_value
as we did above, you will need to be careful not to break this behaviour!
Let’s look again at the code from above:
The problem with this code is that if the field "foo"
is not supplied during a PATCH
request - which is entirely valid, since a PATCH
should not require all fields - then the validation will fail. To fix that, we can make the validation optional dependant upon whether the field exists:
Non-ORM Endpoints §
Tastypie supported non-ORM endpoints through overriding various methods in the Resource
class, such as obj_get
, obj_get_list
, get_object_list
. Fortunately this is simpler in Django REST Framework. For read-only non-ORM endpoints, you simply create a view set which inherits from ViewSet
(rather than ModelViewSet
, which is used for ORM data) and then override the list
method to return a Response
object of your choice.
Add resource_uri
to each endpoint §
While in Tastypie each resource has a resource_uri
included by default, in Django REST Framework you must explicitly add this field to each serializer. Further, by default Django REST Framework calls this field url
.
First update the settings.py
to set default value for URL_FIELD_NAME
to resource_uri
:
Then update all of your serializers to add the "resource_uri"
field:
Using non-PK for URL §
By default Django REST Framework HyperlinkedModelSerializer
uses the pk
field of the Django model for the URL. If you wish to use a different (unique) field for the URL, the steps are as follows.
For example, say we have a Person
model which has a unique username
which we would like to use as the URL, instead of the pk
field.
In the view set, specify the field to use in the lookup_field
attribute:
In the serializer Meta
, use the extra_kwargs
to specify the lookup_field
which the resource_uri
should use.
Note that we are using resource_uri
rather than url
because we changed URL_FIELD_NAME
in the previous section! If you fail to set it as described above you will get the following error:
Note that any other serializers which have foreign keys to the Person
model will also need the extra_kwargs
to be set. For example:
It was thanks to the DRF 3.0 change notes that I was made aware of the extra_kwargs
field which is necessary for these changes!
Infer the non-PK field from the URL §
A problem with the above application of lookup_field
is that it makes update requests cumbersome: you have to specify the PK in both the URL and the payload. For example, in the Person
model from above, if you wanted to PUT
or PATCH
to /api/v1/person/denvercoder9/
the payload would also have to include the username
field, e.g.:
To avoid this, we can modify the to_internal_value
method to infer the non-PK field from the URL, by extracting it from the current context:
This automatically tries to add the data["username"]
field to the incoming request based upon the PK used in the URL, which means it is no longer necessary for the client to pass it through the payload.
Use PUT
to create §
As of DRF v3.0, doing an HTTP PUT to a detail endpoint returns a 404 error by default:
To allow using PUT
to create, there is a mixin provided, which I’ve included below with the necessary imports (thanks to Natim and feliesp):
To use the AllowPUTAsCreateMixin
mixin, simply inherit from it in the relevant view. However, make sure to get the inheritance order correct! Due to the Python Method Resolution Order (MRO), the mixins must come before the ModelViewSet
, else they will have no effect. For example, this is correct:
It will then be possible to PUT
to create.