Using django-rest-framework with tagged items (django-taggit)

Saturday, July 06, 2013

If you have the django-rest-framework in your project to provide a REST-API, and want to expose items, which have a TaggableManager from django-taggit, you have a problem: When adding an item with tags through the REST-API, no tags getting saved or the serializer rejects your request data. I found a way to work around this issue. Don’t know if it’s the best approach, but it is quite straight-forward and works!

Assuming we have a model called Bookmark, which includes the TaggableManager from django-taggit.

bookmarks/models.py:

1
2
3
4
5
6
from django.db import models
from taggit.managers import TaggableManager

class Bookmark(models.Model):
    url = models.URLField("URL")
    tags = TaggableManager(blank=True)

At first it is a good idea to write a ModelSerializer (more info), which maps the fields of our Bookmark-model to the corresponding serializer fields.

bookmark_api/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from bookmarks.models import Bookmark
from rest_framework import serializers
from rest_framework.exceptions import ParseError

class BookmarkSerializer(serializers.ModelSerializer):
    tags = TagListSerializer(blank=True)

    class Meta:
        model = Bookmark
        fields = ('url', 'tags')

As you can see, a custom field (more info) called TagListSerializer is defined for the tags-field. The next step is to implement this TagListSerializer. To to that, we create new class, which inherits from serializers.WritableField and override the methods from_native() and to_native(). This is how we gain control over the serialization- and deserialization-process and define our own logic:

bookmark_api/serializers.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class TagListSerializer(serializers.WritableField):

    def from_native(self, data):
        if type(data) is not list:
            raise ParseError("expected a list of data")     
        return data
    
    def to_native(self, obj):
        if type(obj) is not list:
            return [tag.name for tag in obj.all()]
        return obj

The parameter data of the from_native()-method is a parsed (Python-)list of tags from the HTTP-request’s JSON-object. If the client provides a list like [‘tag1’, ‘tag2’], we’re fine. But if the client provides a string like ‘tag1, tag2’ or a single tag like ‘single'tag’, an exception is raised, resulting in a 400-response to the client. The method to_native() expects either the TaggableManager-object or a list of tags and returns the tag-list.

At the end we have to define a ModelViewSet (more info), which implements the complete set of default read and write operations for our Bookmark model. We have to override the post_save method, to finally save the tags to the previously saved Bookmark-model.

bookmarks_api/views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from rest_framework import viewsets
from bookmarks.models import Bookmark
from bookmarks_api.serializers import BookmarkSerializer

class BookmarkViewSet(viewsets.ModelViewSet):
    serializer_class = BookmarkSerializer
    queryset = Bookmark.objects.all()
    
    def post_save(self, bookmark, *args, **kwargs):
        if type(bookmark.tags) is list:
            # If tags were provided in the request
            saved_bookmark = Bookmark.objects.get(pk=bookmark.pk)
            for tag in bookmark.tags:
                saved_bookmark.tags.add(tag)

This is my approach of implementing django-taggit tags in conjunction with django-rest-framework. If you know a better way to deal with the TaggableManager please leave a reply below!