My personal DRF serializer notes
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 intoJSON
,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
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.