When building APIs with Django REST Framework (DRF), flexibility matters. Learn how to dynamically choose the right serializer class based on user roles, allowing you to tailor information exposure. Whether you’re a seasoned developer or just starting, this technique will enhance your toolkit and help you create more adaptable APIs.
Practically and for most cases that I have observed, it so happens that an application tends to expose only required information based on what a client asks (requests) the resource. Hence the application needs different serializer classes based on the requirement at the time of writing APIs in DRF.
For instance, to get the list of users in pagination style, you may simply be interested in showing their names, email IDs, and the organization they’re working for. But for a specific user’s detailed view you might want to give other information as well such as addresses, phone numbers, job title, department, record creation/modification time, and record created/modified by whom, etc.
Let’s start with Company and User models defined as follows:
1from django.conf import settings
2from django.db import models
3from django.contrib.auth.models import AbstractUser
4from django.utils.translation import ugettext_lazy as _
5from enumfields import EnumField
6from enumfields import Enum
7
8class SystemUserRole(Enum):
9 SYS_ADMIN = "SYS_ADMIN"
10 SYS_USER = "SYS_USER"
11
12class Common(models.Model):
13 created_at = models.DateTimeField(auto_now_add=True, editable=False)
14 modified_at = models.DateTimeField(auto_now=True, editable=False)
15 created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
16 null=True, db_index=True, editable=False,
17 on_delete=models.SET_NULL, related_name="%(class)s_created")
18 modified_by = models.ForeignKey(settings.AUTH_USER_MODEL,
19 null=True, db_index=True, editable=False,
20 on_delete=models.SET_NULL, related_name="%(class)s_modified")
21
22 class Meta:
23 abstract = True
24 app_label = "sample"
25
26class Company(Common):
27 name = models.CharField(max_length=256, db_index=True, unique=True)
28 email = models.EmailField(db_index=True)
29 phone = models.CharField(max_length=20, blank=True, null=True)
30 address_1 = models.CharField(max_length=256, blank=True, null=True)
31 address_2 = models.CharField(max_length=256, blank=True, null=True)
32 street = models.CharField(max_length=256, blank=True, null=True)
33 city = models.CharField(max_length=256, blank=True, null=True)
34 state = models.CharField(max_length=256, blank=True, null=True)
35 zipcode = models.CharField(max_length=20, blank=True, null=True)
36 country = models.CharField(max_length=256, blank=True, null=True)
37 logo_url = models.TextField(blank=True, null=True)
38
39 class Meta:
40 app_label = "sample"
41
42 def __str__(self):
43 return "{}".format(self.name)
44
45 @property
46 def full_address(self):
47 address_line = ""
48 address_line += self.address_1 if self.address_1 else ""
49 address_line += ", {}".format(self.address_2) if self.address_2 else ""
50 address_line += ", {}".format(self.street) if self.street else ""
51 address_line += ", {}".format(self.city) if self.city else ""
52 address_line += ", {}".format(self.state) if self.state else ""
53 address_line += ", {}".format(self.country) if self.country else ""
54 address_line += ", {}".format(self.zipcode) if self.zipcode else ""
55 return address_line
56
57class User(Common, AbstractUser):
58 email = models.EmailField(_("email address"), unique=True)
59 company = models.ForeignKey(Company, on_delete=models.CASCADE, blank=True, null=True)
60 system_role = EnumField(SystemUserRole, default=SystemUserRole.SYS_USER, blank=True, null=True)
61 registered = models.BooleanField(default=False, db_index=True)
62 avatar = models.CharField(max_length=1024, blank=True, null=True)
63 display_name = models.CharField(max_length=128, blank=True, null=True)
64 job_title = models.CharField(max_length=256, blank=True, null=True)
65 department = models.CharField(max_length=256, blank=True, null=True)
66 phone = models.CharField(max_length=32, blank=True, null=True)
67 address_1 = models.CharField(max_length=256, blank=True, null=True)
68 address_2 = models.CharField(max_length=256, blank=True, null=True)
69 street = models.CharField(max_length=64, blank=True, null=True)
70 city = models.CharField(max_length=64, blank=True, null=True)
71 state = models.CharField(max_length=64, blank=True, null=True)
72 zipcode = models.CharField(max_length=32, blank=True, null=True)
73 country = models.CharField(max_length=64, blank=True, null=True)
74
75 class Meta:
76 app_label = "sample"
77
78 @property
79 def full_address(self):
80 address_line = ""
81 address_line += self.address_1 if self.address_1 else ""
82 address_line += ", {}".format(self.address_2) if self.address_2 else ""
83 address_line += ", {}".format(self.street) if self.street else ""
84 address_line += ", {}".format(self.city) if self.city else ""
85 address_line += ", {}".format(self.state) if self.state else ""
86 address_line += ", {}".format(self.country) if self.country else ""
87 address_line += ", {}".format(self.zipcode) if self.zipcode else ""
88 return address_line
User model has been extended from Common & AbstractUser models and has a field company refers to Company which means mostly each user (except is_superuser, is_staff members) should belong to a company with at least SYS_USER role.
Now, let’s create a few serializer classes for these models as follows:
1from rest_framework import serializers
2from enumfields.drf.serializers import EnumSupportSerializerMixin
3
4from .models import User, Company
5
6class CompanySerializer(serializers.ModelSerializer):
7 full_address = serializers.CharField(read_only=True)
8
9 class Meta:
10 model = Company
11 fields = ("id", "name", "email", "phone", "full_address", "logo_url")
12
13class CompanyDetailSerializer(serializers.ModelSerializer):
14 full_address = serializers.CharField(read_only=True)
15
16 class Meta:
17 model = Company
18 fields = "__all__"
19
20class UserSerializer(serializers.ModelSerializer):
21 company_details = CompanySerializer(source="company", read_only=True)
22 full_address = serializers.CharField(read_only=True)
23
24 class Meta:
25 model = User
26 fields = ("id", "first_name", "last_name", "email", "company_details", "company", "full_address", )
27 extra_kwargs = {
28 'company': {'write_only': True},
29 }
30
31class UserDetailSerializer(EnumSupportSerializerMixin, serializers.ModelSerializer):
32 company_details = CompanySerializer(source="company", read_only=True)
33 full_address = serializers.CharField(read_only=True)
34
35 class Meta:
36 model = User
37 exclude = ("password", "groups", "user_permissions", "date_joined", )
38 extra_kwargs = {
39 'company': {'write_only': True},
40 }
Here, we created two serializer classes for each model, one for listing purpose and another one for detailed lookup.
Now, your application wants to use UserSerializer when the client hits the /users/ API endpoint and UserDetailSerializer for the/users/{pk}/ API endpoint. Under a viewset you can do this by overriding get_serializer_class method. You can introduce a new variable serializer_action_classes along with serializer_class under a viewset which maps the viewset action name to serializer class. Now, your overridden get_serializer_class will look like,
If an API endpoint matches to viewset action (e.g, /users/ API endpoint to listviewset action) then the serializer class will be used if it’s defined in serializer_action_classes variable else it will fall back to use serializer_class, the default one. You can create mixin for this so that you can use the functionality in other viewsets too without overriding get_serializer_class method under each viewset. So, the mixin GetSerializerClassMixin would look like as follows:
1class GetSerializerClassMixin(object):
2 def get_serializer_class(self):
3 """
4 A class which inhertis this mixins should have variable
5 `serializer_action_classes`.
6 Look for serializer class in self.serializer_action_classes, which
7 should be a dict mapping action name (key) to serializer class (value),
8 i.e.:
9 class SampleViewSet(viewsets.ViewSet):
10 serializer_class = DocumentSerializer
11 serializer_action_classes = {
12 'upload': UploadDocumentSerializer,
13 'download': DownloadDocumentSerializer,
14 }
15 @action
16 def upload:
17 ...
18 If there's no entry for that action then just fallback to the regular
19 get_serializer_class lookup: self.serializer_class, DefaultSerializer.
20 """
21 try:
22 return self.serializer_action_classes[self.action]
23 except (KeyError, AttributeError):
24 return super().get_serializer_class()
Now, you just have to inherit GetSerializerClassMixin class in your viewset classes and just mention the serializer_action_class variable so that our final viewset code would like:
1from rest_framework import viewsets
2from .mixins import GetSerializerClassMixin
3from .models import User, Company, SystemUserRole
4from .serializers import (
5 CompanySerializer,
6 CompanyDetailSerializer,
7 UserSerializer,
8 UserDetailSerializer,
9)
10
11class CompanyViewSet(GetSerializerClassMixin, viewsets.ModelViewSet):
12 """
13 API endpoint that allows companies to be viewed or edited.
14 """
15 queryset = Company.objects.all()
16 serializer_class = CompanyDetailSerializer
17 serializer_action_classes = {
18 'list': CompanySerializer,
19 }
20 filterset_fields = ("country", "state", "city", )
21 search_fields = ("name", "email", )
22 ordering_fields = ("name", "country", )
23 ordering = ("-created_at", )
24
25class UserViewSet(GetSerializerClassMixin, viewsets.ModelViewSet):
26 """
27 API endpoint that allows users to be viewed or edited.
28 """
29 queryset = User.objects.all()
30 serializer_class = UserDetailSerializer
31 serializer_action_classes = {
32 'list': UserSerializer,
33 }
34 filterset_fields = ("country", "state", "city", "zipcode", "company", )
35 search_fields = ("first_name", "last_name", "email", )
36 ordering_fields = ("first_name", "last_name", "email", )
37 ordering = ("-created_at", )
Let’s say Frank and Joe belong to a company called RainDrops with roles SYS_ADMIN and SYS_USER respectively. Note that this allows Frank to manage all users under RainDrops company. Therefore, Frank is able to see full details (UserDetailSerializer data) of all users from RainDrops company whereas Joe is able to see only limited info (UserSerializer data). Also, Frank and Joe both are able to see only limited information of a user that is not part of RainDrops company.
Okay, this is a very special case which I came across during one of my projects. For the assignment, I had to write custom logic by overriding get_serializer_class method, had to dig into DRF source code by taking help from some of code through drf-extensions as well.
1from rest_framework import viewsets
2from .mixins import GetSerializerClassMixin
3from .models import User, Company, SystemUserRole
4from .serializers import (
5 CompanySerializer,
6 CompanyDetailSerializer,
7 UserSerializer,
8 UserDetailSerializer,
9)
10
11class CompanyViewSet(GetSerializerClassMixin, viewsets.ModelViewSet):
12 """
13 API endpoint that allows companies to be viewed or edited.
14 """
15 queryset = Company.objects.all()
16 serializer_class = CompanyDetailSerializer
17 serializer_action_classes = {
18 'list': CompanySerializer,
19 }
20 filterset_fields = ("country", "state", "city", )
21 search_fields = ("name", "email", )
22 ordering_fields = ("name", "country", )
23 ordering = ("-created_at", )
24
25# class UserViewSet(GetSerializerClassMixin, viewsets.ModelViewSet):
26# """
27# API endpoint that allows users to be viewed or edited.
28# """
29# queryset = User.objects.all()
30# serializer_class = UserDetailSerializer
31# serializer_action_classes = {
32# 'list': UserSerializer,
33# }
34# filterset_fields = ("country", "state", "city", "zipcode", "company", )
35# search_fields = ("first_name", "last_name", "email", )
36# ordering_fields = ("first_name", "last_name", "email", )
37# ordering = ("-created_at", )
38
39class UserViewSet(viewsets.ModelViewSet):
40 """
41 API endpoint that allows users to be viewed or edited.
42 """
43 queryset = User.objects.all()
44 serializer_class = UserSerializer
45 serializer_detail_class = UserDetailSerializer
46 filterset_fields = ("country", "state", "city", "zipcode", "company", )
47 search_fields = ("first_name", "last_name", "email", )
48 ordering_fields = ("first_name", "last_name", "email", )
49 ordering = ("-created_at", )
50
51 def get_serializer_class(self):
52 """
53 Special case to see the user full details.
54 Unless user is request.user or SYS_ADMIN for user's company
55 only show basic details of user.
56 """
57
58 lookup = self.lookup_url_kwarg or self.lookup_field
59 if lookup and lookup in self.kwargs:
60
61 # get detailed endpoint value from url e.g, "/users/2/" => 2
62 user_pk = self.kwargs[lookup]
63 lookup_user = User.objects.filter(pk=user_pk).first()
64
65 # if current user is looking at the details
66 if self.request.user == lookup_user:
67 return self.serializer_detail_class
68
69 # if current user is sys admin of the requested user's company
70 if (self.request.user.system_role == SystemUserRole.SYS_ADMIN and
71 self.request.user.company == lookup_user.company):
72 return self.serializer_detail_class
73
74 return super().get_serializer_class()
75 else:
76 return super().get_serializer_class()
In case you noticed, in the above gist, we’re no longer using GetSerializerClassMixin class for the inheritance plus I changed the value of serializer_class to UserSerializer with serializer_detail_class = UserDetailSerializer as an additional variable. In the above code, we’re getting detailed value from the URL (e.g, /users/2/ => 2) using lookup from kwargs and then using this value we get the user instance. The logic will use serializer_detail_class in two cases: to send the detailed info of a user or use serializer_class for limited info of a user which are as follows:
The credit for using a mixin to choose serializer class per viewset action goes to the two links (posted below), my role was limited to simplifying and explaining it step by step :). Additionally, I covered the special case about limiting delivering data to clients (with Frank and Joe's example). The focus of this article is to deal with what kind of information your APIs want to provide to clients with simple and special use cases based on viewset action.
Add Serializer per action to ViewSets
Django rest framework, use different serializers in the same ModelViewSet
In this article, we’ve explored how to dynamically choose the right serializer class based on user roles. Whether you’re building APIs for a small project or a large-scale application, this technique empowers you to fine-tune information delivery. Remember, flexibility is key!
I hope this will help to deal with the problems similarly I had. The entire sample source code is available in this repository. Thank you for reading the article.
Customizing serializer selection allows you to strike the right balance between simplicity and detail. By tailoring your API responses, you enhance user experiences and optimize data flow. Keep experimenting and adapting—your APIs will thank you!Ready to Transform Your Software Solutions? At Aubergine Solutions, we specialize in crafting robust, scalable software tailored to your unique needs. Whether it’s web development, mobile apps, or custom APIs, our team of experts is here to turn your vision into reality. Learn more about our services and let’s build something amazing together!