7. REST Framework

New in version 1.2.

What would be a modern application without dynamic components? Well, it would not be so modern to begin with. This is why django-hvad provides fully tested and integrated support for Django REST framework.

The philosophy is the same one that is used for Django’s forms, hvad providing the following extensions:

Note

Support for REST framework requires Django REST Framework version 3.1 or newer.


7.1. TranslatableModelSerializer

hvad.contrib.restframework.TranslatableModelSerializer

TranslatableModelSerializer works like ModelSerializer, but can serialize and deserialize translatable fields as well. Their use is very similar, except the serializer must subclass hvad.contrib.restframework.TranslatableModelSerializer:

class BookSerializer(TranslatableModelSerializer):
    class Meta:
        model = Book
        fields = ['title', 'author', 'review']

Notice the difference from a regular serializer? There is none. This BookSerializer will allow serializing and deserializing one Book in one language, correctly introspecting the model to know which fields are translatable.

It is also possible to include the language on the serializer. This is done by default (if no fields is specified), or you may include 'language_code' as part of the field list.

Like TranslatableModelForm, TranslatableModelSerializer can work in either normal mode, or enforce mode. The semantics of both mode are exactly the same as with forms, selecting the way a language is chosen for serializing and deserializing.

  • A serializer is in normal mode if it has no language set. This is the default. In this mode, it will use the language of the instance it is given, defaulting to current language if no instance is specified.
  • A serializer is in enforce mode if is has a language set. This is achieved by giving it a language= argument at instanciation. When in enforce mode, the serializer will always use its own language, disregarding current language and reloading the instance it is given if it has another language loaded.
  • The language can be overriden manually by providing a custom validate() method. This method should set the desired language in data['language_code']. Please refer to REST framework documentation for details on the validate() method.

When the serializer is in normal mode, it is possible to send 'language_code' as part of the serialized representation. More on this below. In enforce mode however, including a language code in a POST, PATCH or PUT request is an error that will raise a ValidationError as appropriate.

All features of regular REST framework serializers work as usual.

Examples

Adding the language to the serialized data, in normal mode:

class BookSerializer(TranslatableModelSerializer):
    class Meta:
        model = Book
        fields = ['title', 'author', 'language_code']

# Now language appears in serialized representation
serializer = BookSerializer(instance=Book.objects.language('ja').get(pk=1))
# => {"title": "星の王子さま", "author": "12", "language_code": "ja" }

# It can also be set explicitly in POST/PUT/PATCH data
print(data['language_code']) # 'fr'
serializer = BookSerializer(data=data)
if serializer.is_valid():
    obj = serializer.save()
    assert obj.language_code == 'fr'

Setting a serializer in enforce mode:

# In enforce mode, serialized data will always use the enforced language
serializer = BookSerializer(instance=Book.objects.untranslated().get(pk=1), language='en')
assert serializer.data['language_code'] == 'en'

# In enforce mode, language is implicit
assert 'language_code' not in request.data
serializer = BookSerializer(data=request.data, language='fr')
if serializer.is_valid():
    obj = serializer.save()
    assert obj.language_code == 'fr'

# In enforce mode, language must not be provided in data
assert 'language_code' in request.data
serializer = BookSerializer(data=request.data, language='fr')
assert not serializer.is_valid()

Manually overriding deserialized language:

class UserBookSerializer(TranslatableModelSerializer):
    def validate(self, data):
        # assuming you made a custom User model that has an associated
        # preferences object including the user's preferred language
        data = super(UserBookSerializer, self).validate(data)
        data['language_code'] = self.context['request'].user.preferences.language
        return data

    class Meta:
        model = Book

7.2. HyperlinkedTranslatableModelSerializer

hvad.contrib.restframework.HyperlinkedTranslatableModelSerializer

The HyperlinkedTranslatableModelSerializer is equivalent to TranslatableModelSerializer, except it outputs hyperlinks instead of ids. There is not much to add here, everything that applies to TranslatableModelSerializer also applies to HyperlinkedTranslatableModelSerializer, except it uses REST framework’s HyperlinkedModelSerializer semantics.


7.3. TranslationsMixin

hvad.contrib.restframework.TranslationsMixin

This mixin is another approach to handling translations for your REST api. With TranslatableModelSerializer, a relevant language is made visible, which is perfect for translation-unaware client-side applications. TranslationsMixin takes the other approach: it exposes all translations at once, letting the client-side application choose or handle translations the way it wants. This is most useful for admin-type applications.

Use is very simple: mix it into a regular serializer:

from rest_framework.serializers import ModelSerializer

class BookSerializer(TranslationsMixin, ModelSerializer):
    class Meta:
        model = Book

obj = Book.objects.untranslated().prefetch_related('translations').get(pk=1)
serializer = BookSerializer(instance=obj)
pprint(serializer.data)
# {'author': '1',
#  'id': 1,
#  'translations': {'en': {'title': 'The Little Prince'},
#                   'fr': {'title': 'Le Petit Prince'}}}

Note

For performance, you should always prefetch the translations like in the above example, otherwise the serializer will have to fetch them for each object independently, resulting in a large number of queries.

Writing is supported as well. It takes a dictionary of translations, the very same format it outputs. Existing translations will be updated, missing translations will be created. Any existing translation that is not in the data will be deleted.

For convenience, you can include both the translations dictionary and translated fields in the same serializer. This can be handy if only some parts of your application care about all the translations. For instance, a book listing might just want the title in the preferred language, while the book editing dialog allows editing all languages. In this case, direct translated fields will be read-only, use the translations dictionary for updating.

It is possible to override the representation of translations. This is done by specifying a custom serializer on the meta:

from rest_framework import serializers

class BookTranslationSerializer(serializers.ModelSerializer):
    class Meta:
        exclude = ['subtitle', 'cover']

class BookSerializer(TranslationsMixin, serializers.ModelSerializer):
    class Meta:
        model = Book
        translations_serializer = BookTranslationSerializer

In case advanced customisation of translations is required, be aware that your custom translation serializer is handed the full object. This allows building computed fields using both translated and untranslated data.

However, it can interfer with some field types, most notable related fields, which expect the actual translation model. Hvad handles this automatically in its default translation serializer. You can inherit this handling by making your own translation serializer a subclass of hvad.contrib.restframework.NestedTranslationSerializer.