Serializers
Speed up serializers queries
Let’s say we have model Travel with many related fields:
class Travel(models.Model):
tags = models.ManyToManyField(
Tag,
related_name='travels', )
route_places = models.ManyToManyField(
RoutePlace,
related_name='travels', )
coordinate = models.ForeignKey(
Coordinate,
related_name='travels', )
date_start = models.DateField()And we want to build CRUD in /travels via view ViewSet.
Here is the simple viewset:
class TravelViewset(viewsets.ModelViewSet):
queryset = Travel.objects.all()
serializer_class = TravelSerializerProblem with this ViewSet is we have many related fields in our Travel model, so Django will hit db for every Travel instance. We can call select_related and prefetch_related directly in queryset attribute, but what if we want to separate serializers for list, retrieve, create.. actions of ViewSet.
So we can put this logic in one mixin and inherit from it:
class QuerySerializerMixin(object):
PREFETCH_FIELDS = [] # Here is for M2M fields
RELATED_FIELDS = [] # Here is for ForeignKeys
@classmethod
def get_related_queries(cls, queryset):
# This method we will use in our ViewSet
# for modify queryset, based on RELATED_FIELDS and PREFETCH_FIELDS
if cls.RELATED_FIELDS:
queryset = queryset.select_related(*cls.RELATED_FIELDS)
if cls.PREFETCH_FIELDS:
queryset = queryset.prefetch_related(*cls.PREFETCH_FIELDS)
return queryset
class TravelListSerializer(QuerySerializerMixin, serializers.ModelSerializer):
PREFETCH_FIELDS = ['tags'']
RELATED_FIELDS = ['coordinate']
# I omit fields and Meta declare for this example
class TravelRetrieveSerializer(QuerySerializerMixin, serializers.ModelSerializer):
PREFETCH_FIELDS = ['tags', 'route_places']Now rewrite our ViewSet with new serializers
class TravelViewset(viewsets.ModelViewSet):
queryset = Travel.objects.all()
def get_serializer_class():
if self.action == 'retrieve':
return TravelRetrieveSerializer
elif self.action == 'list':
return TravelListSerializer
else:
return SomeDefaultSerializer
def get_queryset(self):
# This method return serializer class
# which we pass in class method of serializer class
# which is also return by get_serializer()
q = super(TravelViewset, self).get_queryset()
serializer = self.get_serializer()
return serializer.get_related_queries(q)
Updatable nested serializers
Nested serializers by default don’t support create and update. To support this without duplicating DRF create/update logic, it is important to remove the nested data from validated_data before delegating to super:
# an ordinary serializer
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ('phone', 'company')
class UserSerializer(serializers.ModelSerializer):
# nest the profile inside the user serializer
profile = UserProfileSerializer()
class Meta:
model = UserModel
fields = ('pk', 'username', 'email', 'first_name', 'last_name')
read_only_fields = ('email', )
def update(self, instance, validated_data):
nested_serializer = self.fields['profile']
nested_instance = instance.profile
# note the data is `pop`ed
nested_data = validated_data.pop('profile')
nested_serializer.update(nested_instance, nested_data)
# this will not throw an exception,
# as `profile` is not part of `validated_data`
return super(UserDetailsSerializer, self).update(instance, validated_data)In the case of many=True, Django will complain that ListSerializer does not support update. In that case, you have to handle the list semantics yourself, but can still delegate to nested_serializer.child.
Order of Serializer Validation
In DRF, serializer validation is run in a specific, undocumented order
- Field deserialization called (
serializer.to_internal_valueandfield.run_validators) serializer.validate_[field]is called for each field.- Serializer-level validators are called (
serializer.run_validationfollowed byserializer.run_validators) - Finally,
serializer.validateis called to complete validation.
Getting list of all related children objects in parent’s serializer
Assume that, we implement a simple API and we have the following models.
class Parent(models.Model):
name = models.CharField(max_length=50)
class Child(models.Model):
parent = models.ForeignKey(Parent)
child_name = models.CharField(max_length=80)And we want to return a response when a particular parent is retrieved via API.
{
'url': 'https://dummyapidomain.com/parents/1/',
'id': '1',
'name': 'Dummy Parent Name',
'children': [{
'id': 1,
'child_name': 'Dummy Children I'
},
{
'id': 2,
'child_name': 'Dummy Children II'
},
{
'id': 3,
'child_name': 'Dummy Children III'
},
...
],}
For this purpose, we implement the corresponding serializers like this:
class ChildSerializer(serializers.HyperlinkedModelSerializer):
parent_id = serializers.PrimaryKeyRelatedField(queryset=Parent.objects.all(),source='parent.id')
class Meta:
model = Child
fields = ('url','id','child_name','parent_id')
def create(self, validated_data):
subject = Child.objects.create(parent=validated_data['parent']['id'], child_name=validated_data['child_name'])
return child
class ParentSerializer(serializers.HyperlinkedModelSerializer):
children = ChildSerializer(many=True, read_only=True)
class Meta:
model = Course
fields = ('url','id','name','children')To make this implementation work properly we need to update our Child model and add a related_name to parent field. Updated version of our Child model implementation should be like this:
class Child(models.Model):
parent = models.ForeignKey(Parent, related_name='children') # <--- Add related_name here
child_name = models.CharField(max_length=80)By doing this, we’ll be able to get the list of all related children objects in parent’s serializer.