How to use Django REST Framework (DRF) serializers more efficiently and effectively.
Custom Data Validation
DRF enforces data validation in the deserialization process, which is why we need to call is_valid() before accessing the validated data. if the data is invalid, errors are then appended to the serializer's error property and a ValidationError is thrown.
There are two types of custom data validators:
1. Custom field
2. Object-level
Custom field validation
Custom field validation allows us to validate a specific field. We can use it by adding the validate<fieldname> method to our serializer like so:
from rest_framework import serializersfrom examples.models import MovieclassMovieSerializer(serializers.ModelSerializer):classMeta: model = Movie fields ='__all__'defvalidate_rating(self,value):if value <1or value >10:raise serializers.ValidationError('Rating has to be between 1 and 10.')return value
Object-level validation
Sometimes we'll have to compare fields with one another in order to validate them. This is when we should use the object-level validation approach.
We should avoid accessing additional fields in the custom field validator.
Function Validators
If we use the same validator in multiple serializers, we can create a function validator instead of writing the same code over and over again.
Custom Outputs
Two of the most useful functions inside the BaseSerializer class that we can override are to_representation() and to_internal_value() . By overriding them, we can change the serialization and deserialization behavior, respectively, to append additional data, extract data, and handle relationships.
1. to_representations() allows us to change the serialization output
2. to_internal_value() allows us to change the deserialization output
to_representation()
Now, let's say we want to add a total likes count to the serialized data.
to_internal_value()
Suppose the services that use our API appends unnecessary data to the endpoint when creating resources:
Serializer Save
Calling save() will either create a new instance or update an existing instance, depending on if an existing instance was passed when instantiating the serializer class:
Passing data directly to save
Sometimes we'll want to pass additional data at the point of saving the instance. This additional data might include information like the current user, the current time, or request data.
We can do so by including additional keyword arguments when calling save() .
Serializer Context
There are some cases when we need to pass additional data to our serializers. we can do that by using the serializer context property. We can then use this data inside the serializer such as to_representation or when validating data. We can pass data as a dictionary via the context keyword:
Source Keyword
The DRF serializer comes with the source keyword, which is extremely powerful and can be used in multiple case scenarios. We can use it to:
Rename serializer output fields
Attach serializer function response to data
Fetch data from one-to-one models
Rename serializer output fields
To rename a serializer output field we need to add a new field to our serializer and pass it to fields property.
Attach Serializer function response to data
We can use source to add a field that equals to function's return.
get_full_name() is a method from the Django User model that concatenates user.first_name and user.last_name .
Append data from one-to-one models
Now let's suppose we also wanted to include our user's bio and birth_date in UserSerializer . We can do that by adding extra fields to our serializer with the source keyword.
SerializerMethodField
SerializerMethodField is a read-only field, which gets its value by calling a method on the serializer class that it is attached to. SerializerMethodFeild gets its data by calling get_<field_name> .
Different Read and Write Serializers
If our serializers contain a lot of nested data, which is not required for write operations, we can boost our API performance by creating separate read and write serializers.
We do that by overriding the get_serializer_class() method in our ViewSet like so:
Read-only Fields
Serializer fields come with the read_only option. By setting it to True , DRF includes the field in the API output, but ignores it during create and update operations:
If we want to set multiple fields to read_only we can specify them using read_only_fields in Meta like so:
Settings fields like id , created_date , etc to read-only will give us a performance boost during write operations.
Nested Serializers
There are two different ways of handling nested serialization with ModelSerializer :
1. Explicit definition
2. Using the depth field
Explicit definition
The explicit definition works by passing an external Serializer as a field to our main serializer.
Using the depth field
When it comes to nested serialization, the depth field is one of the most powerful features.
The downside is that we have no control over a child's serializers. Using depth will include all fields obn the children.
Summary
Method
Validating data at the field level
validate<fieldname>
Validating data at the object level
validate
Customizing the serialization and
deserialization output
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def validate(self, data):
if data['us_gross'] > data['worldwide_gross']:
raise serializers.ValidationError('worldwide_gross cannot be bigger than us_gross')
return data
def is_rating(value):
if value < 1:
raise serializers.ValidationError('Value cannot be lower than 1.')
elif value > 10:
raise serializers.ValidationError('Value cannot be higher than 10')
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
rating = IntegerField(validators=[is_rating])
...
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['likes'] = instance.liked_by.count()
return representatio
{
"info": {
"extra": "data",
...
},
"resource": {
"id": 1,
"title": "C++ with examples",
"content": "This is the resource's content.",
"liked_by": [
2,
3
],
"likes": 2
}
}
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_internal_value(self, data):
resource_data = data['resource']
return super().to_internal_value(resource_data)
# this creates a new instance
serializer = MySerializer(data=data)
# this updates an existing instance
serializer = MySerializer(instance, data=data)
# Keep in mind that values passed to save() won't be validated.
serializer.save(owner=request.user)
from rest_framework import serializers
from examples.models import Resource
resource = Resource.objects.get(id=1)
serializer = ResourceSerializer(resource, context={'key': 'value'})
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['key'] = self.context['key']
return representation
class UserSerializer(serializers.ModelSerializer):
# is_active field will be name as active
active = serializers.BooleanField(source='is_active')
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_staff', 'active']
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name')
class Meta:
model = User
fields = ['id', 'username', 'full_name', 'email', 'is_staff', 'active']
class UserSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='userprofile.bio')
birth_date = serializers.DateField(source='userprofile.birth_date')
class Meta:
model = User
fields = [
'id', 'username', 'email', 'is_staff',
'is_active', 'bio', 'birth_date'
] # note we also added the new fields here
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = '__all__'
def get_full_name(self, obj):
return f'{obj.first_name} {obj.last_name}'
from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelWriteSerializer, MyModelReadSerializer
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update", "destroy"]:
return MyModelWriteSerializer
return MyModelReadSerializer
from rest_framework import serializers
class AccountSerializer(serializers.Serializer):
id = IntegerField(label='ID', read_only=True)
username = CharField(max_length=32, required=True)
from rest_framework import serializers
class AccountSerializer(serializers.Serializer):
id = IntegerField(label='ID')
username = CharField(max_length=32, required=True)
class Meta:
read_only_fields = ['id', 'username']
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta:
model = Comment
fields = '__all__'
from rest_framework import serializers
class ModelASerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = '__all__'
depth = 2
# Output
{
"id": 1,
"content": "A content",
"model_b": {
"id": 1,
"content": "B content",
"model_c": {
"id": 1,
"content": "C content"
}
}
}