My personal DRF serializer notes
5 min read

My personal DRF serializer notes

My personal DRF serializer notes
Photo by Prophsee Journals / Unsplash

One of the essential Django libraries and packages you will spend most of your time working with as a Django backend developer is Django Rest Framework which is mainly used for designing and writing Django REST APIs.

DRF (shorthand name for Django Rest Framework) provides Django with a bunch of superpowers and functionalities that will help you as a backend developer in your journey making Django APIs, among the components that come with DRF are serializers.

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
-DRF official documentation

Serializers in simpler words are kind of translators between your API and the outer space, their main role is to make sure that your API and anyone who communicates with it speaks the same language and understands each other well through two fundamental operations "serialization" and "deserialization".

Today we gonna discover a small part of what serializers offer and what I personally find myself using most of the time in some specific use cases when I'm working on Django backends.

Here is what we are going to learn in a nutshell:

1 - Fields
  - Serializer Method Field
  - Read Only Field
  - Custom Field Validation
  - Using Multiple Serializers
2 - Data
  - Custom Data Validation
  - Custom Output with `to_representation`()
  - Custom Input with `to_internal_value`().
3 - Keywords
  - The `source` Keyword
  - The `context` Keyword

GitHub - Abdenasser/dr_scaffold: scaffold django rest apis like a champion 🚀
scaffold django rest apis like a champion 🚀. Contribute to Abdenasser/dr_scaffold development by creating an account on GitHub.
save time creating Django APIs with this awesome tool and support the repository by dropping a star ⭐

Disclaimer: These notes have been written for the only purpose of reminding my future self about how I can achieve some specific functionality and use DRF in some specific use cases, these are not by any means learning materials you can depend on to learn DRF as it already assumes that I/you know DRF basics.

1- Fields

Serializer Method Field

This is a read-only field. It gets its value by calling a method get_<field_name> on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object, Example:

class UserSerializer(serializers.ModelSerializer):
    days_since_joined = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = '__all__'

    def get_days_since_joined(self, obj):
        return (now() - obj.date_joined).days

Read Only Field

Read-only fields are included in the API output, but should not be included in the input during create or update operations. Any read_only fields that are incorrectly included in the serializer input will get ignored, Example:

class AccountSerializer(serializers.Serializer):
    id = IntegerField(label='ID', read_only=True)

Custom Field Validation

Validation in DRF serializers is handled a little differently to how validation works in Django's ModelForm class, With ModelForm the validation is performed partially on the form, and partially on the model instance, with DRF the validation is performed entirely on the serializer class.

Let's take an example where we want to validate if students ages are between 12 and 18:

class StudentSerializer(serializers.ModelSerializer):
    ...
    def validate_age(self, age):
        if age > 18 or age < 12:
            raise serializers.ValidationError('Age has to be between 12 and 18.')
        return age

Using Multiple Serializers

You can override the get_serializer_class() of your ViewSet when for example you want to use a different Serializer in your create and update actions like the following:

class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()

    def get_serializer_class(self):
        if self.action in ["create", "update"]:
            return WriteSerializer
        return ReadSerializer

2- Data

Custom Data Validation

Besides Custom Field Validation, there are two additional ways we can use to validate our data, when for example we need to compare some of our fields between each other the best way to do that is on the object level, like this:

class OrderSerializer(serializers.ModelSerializer):
    ...
    def validate(self, data):
        if data['discount_amount'] > data['total_amount']:
            raise serializers.ValidationError('discount cannot be bigger than the total amount')
        return data

To keep our code DRY when a validation logic is repeated multiple times in some serializers, we can extract it to a function, example:

def is_valid_age(value):
    if age < 12:
        raise serializers.ValidationError('age cannot be lower than 12.')
    elif age > 18:
        raise serializers.ValidationError('age cannot be higher than 18')

Then pass it like this in the other serializers:

class AnotherSerializer(serializers.ModelSerializer):
    age = IntegerField(validators=[is_valid_age])

Custom Output with to_representation()

When we want to customize the output right before it is sent we can use to_representation(), imagine for example we have an output like the following after serialization is done:

{
  "id": 1,
  "username": "abdenasser",
  "bio": "Hey ... you already know!",
  "followed_by": [2, 3]
}

and we want to add a total followers count to it... we can simply do:

class ResourceSerializer(serializers.ModelSerializer):
    ...
    def to_representation(self, instance):
        representation = super().to_representation(instance)
        representation['followers'] = instance.followed_by.count()
        return representation

Then we'll get:

{
  "id": 1,
  "username": "abdenasser",
  "bio": "Hey ... you already know!",
  "followed_by": [2, 3],
  "followers": 2
}

Custom Input with to_internal_value()

Let's say that our API is expecting some input from a 3rd party service and we are only interested in a chunk of that input, then we can use to_internal_value() as follow:

class SomeSerializer(serializers.ModelSerializer):
    ...
    def to_internal_value(self, data):
        useful_data = data['useful']
        return super().to_internal_value(useful_data)

3- Keywords

The source Keyword

In essence, we can use source in a field like this

field_name = serializers.SomeFieldType(source='prop')

Where prop could be a call for a function that returns some value, or a property that exists in a related model like ...(source='author.bio') or even a serializer field that we want to rename in output.

We can also attach a whole object to a field with source='*' if you need.

The context Keyword

We can provide arbitrary additional context by passing a context argument when instantiating a serializer. For example:

resource = Resource.objects.get(id=1)
serializer = ResourceSerializer(resource, context={'key': 'value'})

The context dictionary can then be used within any serializer field logic, such as a custom .to_representation() method, by accessing the self.context attribute.

def to_representation(self, instance):
    representation = super().to_representation(instance)
    representation['key'] = self.context['key']

    return representation

Final words:

DRF has a very good documentation which you can find and read here, try to spend some time on it and use it as a fall back reference any time you feel that things started getting complicated in your serializers.