Skipping Django serialization of rarely changing objects

Skipping Django serialization of rarely changing objects

For database objects that rarely change but are often requested using a RESTful API, repeated serialization is wasteful. We show you how serialization can be skipped using the HTTP header If-Modified-Since.
As an example, consider a very simplified version of the Django model used by kubo to represent a store:

Using the Django REST framework, it is simple to create a serializer and a viewset to retrieve details about all stores available to kubo ordered by name:

Technically this works fine. However, even though any of the fields of a Store rarely change, each call to a /stores/123/ API route has to serialize the same data over and over again.

But what if the Kubo app remembers when it last queried the Kubo server for a store, and could request to send the full data only if anything changed?

Luckily the HTTP protocol already provides the means to do just that by specifying If-Modified-Since header. In case the data changed, the result should be a HTTP 200 (“ok”) and the full serialized data. In case nothing changed since the last request, the result should be a HTTP 304 (“not modified”) without any further data. In this case the app will know that the current data it has already stored locally are still up to date.

To enable such behavior, we first have to remember when a Store was last changed. This can be done by adding a DateTimeField to Store with auto_now=True:

last_modified = models.DateTimeField(auto_now=True)

The auto_now means that the value of last_modified is automatically updated by Django to the current time each time Store.save() is called.

Next we have to tell our viewset to do a full serialization only in case anything changed.

Although Django already provides a decorator @last_modified for conditional view processing, it cannot easily be used in a REST viewset because the viewset has to look at the query parameters to know which objects to retrieve. By then, the decorator has already been passed.

Closer analysis of the ReadOnlyModeViewset reveals that in order to obtain a single object, the retrieve() method from the RetrieveModelMixin is called:

So we need to overload this and check for the If-Modified-Since header.

Getting the header from the request is as simple as

if_modified_since_value = request.headers.get("If-Modified-Since")

This header uses the date format described in RFC2616, for example:

Wed, 7 Apr 2021 13:11:23 GMT

For parsing (and formatting) this, Python provides email.utils.parsedate_to_datetime (resp. format_datetime).

We also want to handle any date formatting errors as HTTP 400 (“bad request”).

In case no modifications have happened the result is a simple

return Response({}, HTTP_304_NOT_MODIFIED)

as opposed to the previous, more computational intensive

serializer = self.get_serializer(instance)
return Response(serializer.data)

So the improved retrieve() is:

Of course, instead of providing this only for the StoreViewSet it is sensible to make a more general solution. In practice one would create a reusable class like ModifiedReadOnlyModelViewset that extends the standard ReadOnlyModelViewSet. Then viewsets for any appropriate models can derive from it.

Conclusion

With a few lines of code, viewsets of the Django REST framework can skip serialization for unmodified objects provided the client specified the HTTP standard header If-Modified-Since. This reduces the computational effort needed to compute the result and the size of the payload sent over the network.