What's inside
This is the last part of my tutorial for the Django REST framework.
Be sure to catch up with the work we've completed in other parts of the series:
Note: You can find the project code we’re working on in this series here.
In this article, I take a closer look at disabling unnecessary fields and expanding related objects.
I want to present this topic in two ways. The first one will follow the flow of the previous sections of this guide. The second approach will ruin that order - and then bring about a new one. But let’s not get ahead of ourselves. ;)
Selective fields
For this function, we need the drf-dynamic-fields library:
$ pip install drf-flex-fields
Next, we replace the current serializer class (a mixin is also available).
# serializers.py
from rest_flex_fields import FlexFieldsModelSerializer
from rest_framework import serializers
from . import models
class FriendSerializer(FlexFieldsModelSerializer):
owner = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = models.Friend
fields = ('id', 'name', 'owner', 'has_overdue')
From now on, we can point to specific fields we need:
'http://127.0.0.1:8000/api/v1/friends/?fields=id,name'
or list fields that we’d like to omit:
'http://127.0.0.1:8000/api/v1/friends/?omit=has_overdue'
Adding related objects
The simplest way to add related objects is using the right serializer in the relation field.
# serializers.py
class BorrowedSerializer(FlexFieldsModelSerializer):
what = BelongingSerializer()
to_who = FriendSerializer()
class Meta:
model = models.Borrowed
fields = ('id', 'what', 'to_who', 'when', 'returned')
But we need to remember about two things here:
- To avoid burdening our database, let’s fill the default queryset with select_related:
# views.py
class BorrowedViewset(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = models.Borrowed.objects.all().select_related('to_who', 'what')
# ...
2. Relations defined in that way are default only for reading and adding, the saving option will require more work. We can, however, avoid that by using a library that allows expanding fields when needed:
First, we point to the serializers that will be used to expand fields.
class BorrowedSerializer(FlexFieldsModelSerializer):
expandable_fields = {
"what": (BelongingSerializer, {"source": "what"}),
"to_who": (FriendSerializer, {"source": "to_who"})
}
class Meta:
model = models.Borrowed
fields = ('id', 'what', 'to_who', 'when', 'returned')
Next, we need to work with the viewset. At this point, we should change the base class (a mixin is also available) and add a list of expandable fields.
class BorrowedViewset(NestedViewSetMixin, FlexFieldsModelViewSet):
queryset = models.Borrowed.objects.all().select_related('to_who', 'what')
permit_list_expands = ["what", "to_who"]
# ...
NOTE: Make sure to remember about select_related in queryset. It will reduce the burden on the database significantly. You can get similar result with using prefetch_related for the ManyToMany relation.
We can now expand our fields.
'http://127.0.0.1:8000/api/v1/borrowings/?expand=what,to_who'
Here's a different approach
Finally, I wanted to show you another approach that ruins the order we've built so far. I'm going to talk about it using one of the most well-developed DRF extensions, DREST (Dynamic REST). You can find the code for this section in a separate branch of our repository called drest and under the part07-drest tag.
The DREST documentation is very comprehensive, so I'm only going to focus on the most important fragments related to our topic.
Installation and configuration:
$ pip install dynamic-rest
# settings.py
INSTALLED_APPS = [
# ...
"dynamic_rest",
]
One of the first things we should add after the installation is filling out our browsable API with a list of all the available endpoints:
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"dynamic_rest.renderers.DynamicBrowsableAPIRenderer",
],
}
To take full advantage of DREST, we should switch the currently used ModelSerializer class to DynamicModelSerializer. We're also going to change the ModelViewSet class to DynamicModelViewSet.
#serializers.py
from rest_framework import serializers
from dynamic_rest.serializers import DynamicModelSerializer
# ...
class FriendSerializer(DynamicModelSerializer):
# ...
class BelongingSerializer(DynamicModelSerializer):
# ...
class BorrowedSerializer(DynamicModelSerializer):
# ...
# views.py
import django_filters
from django.core.mail import send_mail
from dynamic_rest.viewsets import DynamicModelViewSet
# ...
class FriendViewset(NestedViewSetMixin, DynamicModelViewSet):
# ...
class BelongingViewset(DynamicModelViewSet):
# ...
class BorrowedViewset(NestedViewSetMixin, DynamicModelViewSet):
# ...
Unfortunately, at the moment of wiring this article the library is incompatible with nested routers (which I covered recently). However, it partially realizes this function automatically, allowing us to view ManyToMany relations. For the sake of structure, let's get rid of the code responsible for nesting:
# api.py
from dynamic_rest.routers import DynamicRouter
from rest_framework_extensions.routers import NestedRouterMixin
from core import views as myapp_views
router = DynamicRouter()
friends = router.register(r"friends", myapp_views.FriendViewset)
router.register(r"belongings", myapp_views.BelongingViewset)
router.register(r"borrowings", myapp_views.BorrowedViewset)
We can now begin to view the available functionalities:
Selective fields
Computing the value of specific fields in our serializers can be very costly. That's why we may want to omit them in certain cases or add them only upon request. DREST allows to realize both of these paths.
Deleting fields we don't need is easy. All it takes is adding the parameter exclude[] in our query together with the field's name.
'http://127.0.0.1:8000/api/v1/friends/?exclude[]=has_overdue'
It's worth to pay attention to the change of the returned data default format by the DynamicModelViewSet. Such a structure is in line with the REST best practices.
As for the fields added upon request, we can mark them with the help of the deferred_fields parameter:
class FriendSerializer(DynamicModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = models.Friend
fields = ("id", "name", "owner", "has_overdue")
deferred_fields = ("has_overdue",)
That is how we add the field to the response:
'http://127.0.0.1:8000/api/v1/friends/?include[]=has_overdue'
Another key matter is...
Adding related objects
DREST comes with a handy functionality that additionally optimizes query execution time.
Every relation we mark with the DynamicRelationField field can be expanded and the list of related elements will be returned together with the original result.
For example, the following query will include friends and items in the results:
'http://127.0.0.1:8000/api/v1/borrowings?include[]=to_who.\*&include[]=what.\*'
Yes, I do realize that the default name of the borrowed items isn't that great...
If our application has a more complex structure, we can use that method for including objects on any level of nesting. However, we need to remember about the burden we place on the database as it becomes greater with every level.
This article finishes my series about the Django REST Framework where I touched upon almost every aspect of working on the REST API.
Thirsty for more knowledge? Check out other Django tutorials as well!
If that isn't enough for you, you have two options. You can either forge your own path and share your experience with others, or turn to GraphQL (but that's a topic for another series).
But for now... So long and thanks for all the fish!
BTW. If you'd like to learn more about GraphQL, an open-source data query and the manipulation language for APIs, I recommend reading this article: Pros and cons of GraphQL for your project.