Vitamin C

django-rest-framework Serializers

Speed up serializers queries

Let's say we have model Travel with many related fields:

class Travel(models.Model):

    tags = models.ManyToManyField(
        related_name='travels', )
    route_places = models.ManyToManyField(
        related_name='travels', )
    coordinate = models.ForeignKey(
        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 = TravelSerializer

Problem 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

    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
            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

  1. Field deserialization called (serializer.to_internal_value and field.run_validators)
  2. serializer.validate_[field] is called for each field.
  3. Serializer-level validators are called (serializer.run_validation followed by serializer.run_validators)
  4. Finally, serializer.validate is called to complete validation.

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': '',
'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='')

    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.

Got any django-rest-framework Question?