Django REST Framework is an amazing librairy built on top of Python's most powerful web framework Django. It is one of the best features to add to Django. But as always, nothing is perfect and to get the best out of this framework, you have to tinker with a few things around it. There's always one thing that annoys me when I use this framework, that's how serializers behave when it comes to retrieving and validating data.
Let's imagine that we have theses Models:
from rest_framework import serializers
from .models import (Department, Employee,)
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = '__all__'
class EmployeeSerializer(serializers.ModelSerializer):
class Meta:
model = Employee
fields = '__all__'
I assume that you know how to build views or viewsets and write the urlpatterns to use an api which expose Employees and Departments, if not! It's time to go and look at the DRF and Django documentations.
Choices that we can get from this example:
// GET /api/v1/employees/1
{
"id":1,
"employee_name":"mohamed",
"department":1
}
Not so practical to read, we want more information about the department. But so efficient when writing
//POST /api/v1/employees/
{
"id":2,
"employee_name":"another employee_name that we want to create",
"department":1
}
We can play with depth property of the serializer by example
class EmployeeSerializer(serializers.ModelSerializer):
class Meta:
model = Employee
fields = '__all__'
depth = 1
We get this
// GET /api/v1/employees/1
{
"id":1,
"employee_name":"mohamed",
"department":{
"id":1,
"name":"Information technology"
}
}
But if we want to create a new employee_name
we should send all the department object again, here in this example we don't have so much informations but what if we have a model with multiple nested relations, it will be so annoying
// POST /api/v1/employees/
{
"id":1,
"employee_name":"mohamed",
"department":{
"id":1,
"name":"Information technology",
"section":1
},
"issues":[
{
"id":1,
"title":"Optimizing fetch in the frontend"
}
]
// and so go on
}
We can here use the DepartmentSerializer to retrieve the nested relation informations. But this will be a read only field as described in DRF documentation:
By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved "Django REST Framework documentation" you can see here
class EmployeeSerializer(serializers.ModelSerializer):
department = DepartmentSerializer() ## assume you have created the Departement serializer class
class Meta:
model = Employee
fields = '__all__'
def create(self,validated_data):
department_data = validated_data.pop('department')
department = Department.objects.create(**department_data)
employee = Employee(department=department,**validated_data)
employee.save()
return employee
def update(self,instance, validated_data):
department = validated_data.pop('department')
department, created = Department.objects.get_or_create(**department_data)
instance.department = department
instance.save()
return instance
Again, here we have a very simple example, but with multiple nested serializers this will become very long and very hard to maintain. So very good solution to read data but not so easy to write these relations
We can use two serializers to make it easier to read and write data. However, we need to change the behaviour of our views or viewsets. So, to read the data (with GET http verb) or with (list, retrieve viewset actions) we use the ReadSerializer and to write the data with (POSt, PUT http verbs) or with (create, update viewset actions) we use the WriteSerializer.
from rest_framework import serializers
class EmployeeReadSerializer(serializers.ModelSerializer):
department = DepartmentSerializer()
class Meta:
model = Employee
fields = '__all__'
class EmployeeWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Employee
fields = '__all__'
And in the ViewSet or View we can override the get_serializer_class methode to explicitly specify which serializer to use depending on which action we make:
from rest_framework import viewsets
from .models import Employee
from .serializers import (EmployeeReadSerializer, EmployeeWriteSerializer,)
class EmployeeViewSet(viewsets.ModelViewSet):
queryset = Employee.objects.all()
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update", "destroy"]:
return EmployeeWriteSerializer
return EmployeeReadSerializer
This solution may work, but you're writing almost the same serializer twice, and you're overriding the method in the viewset, so there's more code to write. Not very funny.
Finally, we come to the purpose of this article. Here I'm going to tell you about this little trick I found.
Looking at the DRF source-code, especially the ModelSerializer class, I noticed that it automatically generates a PrimaryKeyRelatedField
for the model's relationships. which itself makes the link via the Id.
So, the idea here is to inherit from this Class in order to build our own PrimaryKeyRelatedField:
from collections import OrderedDict
from rest_framework import serializers
## Here I give this class the name of TheAmazingField you can name it whatever you want
class TheAmazingField(serializers.PrimaryKeyRelatedField):
def __init__(self,serializer, many=False,*args,**kwargs) -> None:
super().__init__(*args,**kwargs)
self.serializer = serializer
self.many=many
## When read data we need all the serialized object not only the Id
def to_representation(self,value):
return self.serializer(instance=value, many=self.many).data
"""
I use a small but not mandatory trick to help you reduce typing:
make sure the queryset of the serialized model is automatically
inherited. I am very lazy
"""
def get_queryset(self):
if self.queryset:
return self.queryset
return self.serializer.Meta.model.objects.all()
"""
Get choices is used by the DRF autodoc and expects to_representation()
to return an ID, which causes everything to crash.
We rewrite the trick to use item.pk instead of to_representation()
"""
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
item.pk,
self.display_value(item)
)
for item in queryset
])
"""
DRF skips certain validations when there is only the id,
and as this is not the case here, everything crashes. We disable this.
"""
def use_pk_only_optimization(self):
return False
Then we can use it like this:
class EmployeeSerializer(serializers.ModelSerializer):
department = TheAmazingField(serializer=DepartmentSerializer)
class Meta:
model = Employee
fields = '__all__'
So when we POST a new employee to the endpoint:
// POST api/v1/employees/
{
"department": 1,
"employee_name": "this works"
}
The response of the API will retrieve directly the department object 🎉
{
"id": 1,
"department": {
"id": 1,
"name": "Information Technology"
},
"employee_name": "this works"
}
I don't claim to be an expert on Django's REST Framework, but the solution I've found here has enabled me to make progress on my projects, and when I've presented it to my colleagues, they've been pleased. However, I'd be happy to talk to anyone who would like to improve or criticise this solution.
Also, you can refer to this github repo if you want to see the source-code of the solution I mentioned in this article.