Generic ViewSets — Serializer Context and Hooks

Caronex Labs
Django Rest for Not Beginners
7 min readOct 6, 2020

Hey there! This article is a part of a series on Django Rest Framework for people who already have experience using the framework. However, you do not need any prior context to understand this article. There are some prerequisites about Django Rest Framework itself that you must know, and I will list them out shortly.

This time, I want to address a problem that you may not face too often. When you do come across it though, it can be a little frustrating to resolve. The problem is, passing context to a serializer. Context in this context (:-p) is just some data that you may want to pass on to a serializer.

Like always, here is a list of prerequisites you will need to know about to fully appreciate the article:

  • Generic ViewSets — You can read up the docs here or our article on the topic here.
  • Custom Mixins — Again, docs here and our article on the topic here.
  • Model Serializers — You can read about it in the docs here.
  • Abstract User — This fantastic article by SimpleIsBetterThanComplex explains it best.

Knowing what you’re building makes things clearer, so let me lay the situation out. In the articles before this, we cloned the possible API behind Mediums own Profile Page. In this article however, we will be considering story creation to make the need for passing context more apparent.

Here is the current situation

The user has already filled the story in and is waiting to save the story. When he/she saves the story, a post request with all the data is sent to our API and we are responsible for saving the story under the user who sent the request.

Seems straightforward right…

Again, this series is titled, DRF for not beginners… so I’m going to gloss over the initial setup. Here is what it looks like now.

Here’s how the model would look:

users_module/models.py

Here are the serializers:

users_module/serializers.py

And finally, the views:

If you wish to have a more in-depth view of the project, you can find it here on GitHub.

With this setup, our project now has the capability to store and return user data. Also, the ability to return all the stories authored by the current user.

Now, we begin coding the endpoint to create a new story.

We won’t need to create a new serializer, the current one will work just fine after a few modifications:

class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = '__all__'

# We added the following line.
read_only_fields = ['author', 'story_id']

Earlier, this serializer was only used on a GET request, so all fields were basically read only. Now that we wish to use it for POST requests, we need to specify which fields cannot be modified.

Now let’s look at the view:

# We inherit `CreateModelMixin` in our `StoryViewSet` below.class StoryViewSet(CreateModelMixin, MeMixin, GenericViewSet):
serializer_class = StorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Story.objects.all()
def get_me_config(self):
return {
'instance': self.request.user,
'many': False,
'allowed_methods': ['get']
}
# This is the function we just added def create(self, request, *args, **kwargs):
super(StoryViewSet, self).create(self, request, *args, **kwargs)

This is pretty straightforward too. You don’t need to define the create function explicitly the way I have… We are going to be working in there, which is why I wanted to make it as clear as I could.

We’re almost done. The above code would have been enough if not for one tiny problem. —

“Wait … How do you know who the author is…?”
— Django Developer in Distress (circa. 2020)

What needs to be done is clear. We need to pass the user who sent the request to the serializer so that it can store that user as the author. You can do that easily by passing the user when the serializer is called:

data = StorySerializer(request.data, context={'user':request.user})

The problem is… we don’t have that line in our view.

“ -_- ”
— Django Developer in Distress (circa. 2020)

And to add that single line, you will have to forego the abstraction that Django provides with CreateModelMixin. In order to combat this, Viewsets come with a method called get_serializer_context(). Here’s how that would work:

The view:

class StoryViewSet(CreateModelMixin, MeMixin, GenericViewSet):
serializer_class = StorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Story.objects.all()
def get_me_config(self):
return {
'instance': self.request.user,
'many': False,
'allowed_methods': ['get']
}
# The following function is what we just added def get_serializer_context(self):
context = super(StoryViewSet, self).get_serializer_context()
context.update({"user": self.request.user})
return context

Here, we can pass data to any serializer that may be used in this viewset. In our case, we passed the user object. This will however, need to be handled on the serializer side where we add this user as the author before the object is created. Here’s how that would look:

The serializer:

class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = '__all__'
read_only_fields = ['author', 'story_id']
def create(self, validated_data):
user = self.context['user']
return Story(**validated_data, author=user)

That was simple enough right. Infact, we actually saved a chunk of lines in the view . We don’t have to explicitly define the create method at all anymore.

This is the recommended way of handling serializer context. There is a guideline beneath this method called — “Skinny Controller”.
The idea is that you should keep your views thin and offload processing to either the models or the serializers to support ‘Separation of Concerns’. In doing so, you ensure security of your data such that no outside method can directly change values inside another class.

To be very honest with you… I don’t believe “Skinny Controllers” are a good idea when it comes to Django. In the way that I like to architect my application, my views are like the central hub. The place where all the action takes place. The major reason for this is that I assign a single viewset to deal with a single model. This way, I can encapsulate a single model, its views, its serializers and its router. Sure, other models do get involved sometimes, but that’s inevitable.

When you encapsulate your application this way, a single viewset can have multiple serializers, which is something we discuss in this article. If you offload the logic of selecting which fields to use for which call to the serializers, you break the DRY principle and repeat the same code across multiple serializers. Plus, if you want to perform more complex actions before creating the object, doing them in the view is much more convenient given the available data and methods there.

Basically, the above solution works if you have a limited number of serializers. 2 in our case. But this solution is not extremely scalable.

If this is enough for you… I’m glad I could help. I would however still request you to go through the remaining article, you might learn something you may need someday.

Hooks

Views > Serializers

Mostly because there’s just more data and methods available to you in the view. Recognizing this, Django provides us with a comparatively lesser known set of methods called Hooks. Similar to Lifecycle Hooks you may have seen in frontend frameworks; they allow you to hook into certain automated actions performed by Django and override them. (That sounds so cool…)

There are 3 of them.

Here they are:

Let’s use the perform_create in our current code.

Let’s reset our code to before we implemented context. Here is the code now:

The Serializer:

class StorySerializer(serializers.ModelSerializer):
class Meta:
model = Story
fields = '__all__'
read_only_fields = ['author', 'story_id']

The View:

class StoryViewSet(CreateModelMixin, MeMixin, GenericViewSet):
serializer_class = StorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Story.objects.all()
def get_me_config(self):
return {
'instance': self.request.user,
'many': False,
'allowed_methods': ['get']
}

We have removed the create method from the serializer and the get_serializer_context method from the view.

Our serializer needs no changes, so let’s focus on rewriting the view.

Like I said earlier, these hooks are just methods. I believe that if you have been able to follow me till here, just showing you the code would be more than enough.

Here is our view, everything is the same apart from the last 2 lines:

And that’s it.

No serializer context function. No create method in the serializer. No explicit create method in your view.

And the best part comes now. If you have been following the series, you know that the user model has a field that keeps a count of the number of articles the user has written. For demonstration purposes, we are not calculating that dynamically like we should. So… here is something you can do now that we are using a hook:

class StoryViewSet(CreateModelMixin, MeMixin, GenericViewSet):
serializer_class = StorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Story.objects.all()
def get_me_config(self):
return {
'instance': self.request.user,
'many': False,
'allowed_methods': ['GET']
}
def perform_create(self, serializer):
serializer.save(author=self.request.user)
self.request.user.articles += 1
self.request.user.save()

You can do limitless computation before you save the serializer, while having access to the data and methods inside your view.

It’s pretty fantastic.

This article actually brings us to the end of our series on Generic Viewsets. I hope that I was able to help somehow and maybe even introduce you to new and easier ways to solve problems using Django.

This is not the end at all though. After a short break, we are going to have another series under the same publication but this time we will explore Routers.

Before I end, I would like to tell all of you that when we write articles for a series of blogs, we cut a lot of corners to focus on the important parts… or at least I did. Most times it doesn’t matter too much because the reader knows how to fit what they read here into their own projects. However, if there is any part that you believe needs clarification. Please feel free to reach out to us. And with that…

Thank you, and as always… stay tuned.

- Django Developer Not in Distress (circa. 2020)

--

--