Model & Data Serialization

Serializers are a Django Rest Framework concept that describe converting models to and from JSON, and under what circumstances. They provide a middle man that when paired with views and viewsets can automatically parse and unparse data between requests and responses. They provide a common interface for describing how (de)serialization works through the project and its routes.

DRF Magic provides tooling to make working with serialization even easier by including class decorators to automatically tie serializer classes to the models that they represent. Note that the more you buy into the different features of DRF Magic, the more powerful the automatic serialization features become.

Model Core Serializers

For most models, there is usually one core serializer that is used for a majority of the routes, usually at least for listing of a group of instances and retrieving a specific instance. This means that it may be useful to create a mapping from a model to its main serializer class, so that instances and data of that model/request type can easily and quickly be converted back and forth while having the serializer ⬄ model mapping declared only once, providing good D.R.Y programming habits.

Registering a Main Serializer

DRF Magic has a register_main_serializer function that allows us to easily create this serializer-to-model mapping when we define our serializers based on their defined Meta.model attributes.

drf_magic.serializers.loader.register_main_serializer(serializer_class)

Registers a ModelSerializer as the main serializer for a model type when attached as a decorator on the serializer class declaration. It uses the Meta.model attribute, and each model can only have a single main serializer associated with it.

We can use this in a project in the following manner. Let’s say we have a Person model defined, such as what is below.

from django.db import models


class Person(models.Model):

    first_name = models.CharField(max_length=64, default='')

    last_name = models.CharField(max_length=64, default='')

    occupation = models.CharField(max_length=128, default='programmer')

We can then define a PersonalSerializer that is registered as the main serializer for the above-defined Person model.

# my_app/serializers.py
from drf_magic.serializers.loader import register_main_serializer
from rest_framework import serializers

from .models import Person

@register_main_serializer  # <-- registers the main serializer
class PersonSerializer(serializers.ModelSerializer):
    """Main serializer class for converting Person instances to JSON and back
    """

    class Meta:
        model = Person
        fields = ['id', 'first_name', 'last_name', 'occupation']

Notice how we include the @register_main_serializer around the serializer class definition. That’s the key feature that registers this serializer to the Person class.

We can then pull out this serializer in a declarative, but dynamic way using the load_model_serializer function.

drf_magic.serializers.loader.load_model_serializer(model_class)

Used to safely load a serializer from the main serializer-to-model map

# my_app/views.py
from drf_magic.serializers.loader import load_model_serializer

# Somewhere in our code...
person_serializer_class = load_model_serializer(Person)
assert person_serializer_class == PersonSerializer

Automatically Load Main Serializers

Due to the nature of Django apps and how they are loaded, we must load them when the app configs are ready(). This can be done automatically with the AutoSerializerAppConfig class.

Note

This does not have to be included specifically if your app configs already subclass from drf_magic.MagicAppConfig, as this subclass is already included in that case.

class drf_magic.serializers.loader.AutoSerializerAppConfig(app_name, app_module)

Will attempt to load and register the app’s serializers automatically on app load

In a real example, it may look like the following, continuing with our ongoing example.

# my_app/apps.py

# Preferred method if also using auto model accessors
from drf_magic import MagicAppConfig


class MyAppConfig(MagicAppConfig):
    pass

# --------------------------------

# If needed only for serializers
from drf_magic.serializers.loader import AutoSerializerAppConfig


class MyAppConfig(AutoSerializerAppConfig):
    pass

Serializers in Viewsets

Once we have main model serializers set up for our major models, we can have viewsets automatically use them when we define the viewset’s model attribute. By default, DRF Magic-based viewsets will load the main serializer for its associated views. This functionality comes from the AutoSerializerMixin viewset mixin and its overriding of get_serializer_class.

With all of the following performed, we can then have model instances and data be automatically set and converted based on the mapping of the Person model to the PersonSerializer class.

# my_app/views.py
from rest_framework.responses import Response

from .models import Person

# TODO: Fix this import once its finalized
class PersonViewset(AutoSerializerMixin or MagicViewset):
    """API routes to handle Person model interaction
    """
    model = Person

    # get_serializer_class() --> PersonSerializer

    # builtin actions (list, retrieve, create, delete...) will
    # expect a PersonSerializer-compatible JSON structure for
    # incoming data requests and will convert model instances
    # to the same structure on responses automatically

    @action(detail=True)
    def custom_retrieve(self, **kwargs):
        person = self.get_object()

        # This is where our main serializer setup comes into play
        serializer_class = self.get_serializer_class()

        serializer = serializer_class(instance=person)
        return Response(serializer.data)

Overridding the Main Serializer

In some cases though, it may be deemed necessary to override what serializer to use for a specific viewset action. This may be useful if you have custom actions on the viewset that require different JSON data structures, or just want to have different serializers for a specific action type.

Tip

A scenario that I find myself overriding the core serializer is when implementing a generic main serializer for list-ing, and a detailed serializer when retrieve-ing, or vis versa.

These overrides can be implemented by placing a <action>_serializer_class attribute on the associated viewset where <action> is the action type that the serializer class should be overridden for.

from .serializers import PersonCreationSerializer


class PersonViewset(AutoSerializerMixin or MagicViewset):

    # used during 'create' actions
    create_class_serializer = PersonCreationSerializer

    # used during 'fire' actions
    fire_serializer = FirePersonSerializer

    @action(detail=True)
    def fire(self, **kwargs):
        serializer_class = self.get_serializer_class()
        # ... do something and return a response