59.9 Django REST Framework: Serializers and ViewSets
Right, so you’ve built some models. They’re beautiful. Perfectly normalized, elegant relationships, the whole nine yards. But here’s the problem: the web speaks JSON, not Python objects. Your beautiful BlogPost object is about as useful to a frontend as a chocolate teapot unless you can send it over the wire. That’s where Django REST Framework (DRF) waltzes in, hands you a martini, and says, “I got this.”
At its heart, DRF is about two things: Serializers (turning your models into JSON and back) and ViewSets (controlling the logic for your API endpoints). They work in tandem so you don’t have to write the same tedious CRUD views for every single model.
The Serializer: Your Bouncer and Translator
Think of a Serializer as a multi-talented bouncer for your data. Its jobs are:
- Validation: Checking if the data someone is trying to send you (e.g., in a POST request) is even allowed into the club. Is that
published_dateformatted correctly? Nope. Bounced. - Serialization (to JSON): Taking your complex Python model instances or QuerySets and translating them into a simple, JSON-friendly format for the outside world.
- Deserialization (from JSON): Taking the JSON that was sent to you, validating it, and then turning it back into a complex Python object so you can save it to the database.
Let’s get concrete. Imagine this model:
# models.py
from django.db import models
class BlogPost(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
The serializer for this is almost comically straightforward:
# serializers.py
from rest_framework import serializers
from .models import BlogPost
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
fields = ['id', 'title', 'content', 'is_published', 'created_at']
read_only_fields = ['id', 'created_at'] # These are set by the server, not the client
Boom. Done. ModelSerializer is DRF’s killer feature. It introspects your model and builds a serializer based on it. You get validation rules for free based on your model field definitions (max_length, null, etc.). The fields list is crucial—it’s your explicit allowlist. Never use fields = '__all__' in anything serious; it’s a ticking time bomb. What happens when you add a super_secret_admin_notes field later? Yikes. Be explicit.
Now, what if you want to add a computed field? Something that isn’t in the database? Easy.
class BlogPostSerializer(serializers.ModelSerializer):
read_time_minutes = serializers.SerializerMethodField()
class Meta:
model = BlogPost
fields = ['id', 'title', 'content', 'is_published', 'created_at', 'read_time_minutes']
def get_read_time_minutes(self, obj):
# A silly, simplistic calculation
word_count = len(obj.content.split())
return max(1, round(word_count / 200))
The SerializerMethodField calls a method named get_<field_name> on the serializer. It’s read-only by default, which makes perfect sense.
ViewSets and Routers: Where the Magic (and Laziness) Happens
Okay, you’ve got a serializer. Now you need views to handle GET, POST, PUT, DELETE. You could write them all by hand using DRF’s APIView. It’s a great way to understand what’s happening, but you’ll quickly get bored of writing the same get, post, update, and delete methods over and over.
Enter the ModelViewSet. This single class provides all six default operations: list, create, retrieve, update, partial_update, and destroy. It’s almost absurdly powerful.
# views.py
from rest_framework import viewsets
from .models import BlogPost
from .serializers import BlogPostSerializer
class BlogPostViewSet(viewsets.ModelViewSet):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
That’s it. No, really, that’s the whole view. But wait, we usually don’t want to show unpublished posts to everyone, right? This is where you override the default queryset.
class BlogPostViewSet(viewsets.ModelViewSet):
serializer_class = BlogPostSerializer
def get_queryset(self):
# Show all posts to authenticated users, only published ones to anonymous
if self.request.user.is_authenticated:
return BlogPost.objects.all()
return BlogPost.objects.filter(is_published=True)
This ViewSet now automatically applies this filtering logic to the “list” view and the “retrieve” (detail) view. If an anonymous user tries to retrieve an unpublished post by its ID, they’ll get a 404. Perfect.
Now, how do we turn this class into actual URLs? You could manually wire up URLs for each action, but please don’t. Use a Router. It’s the final piece of the automation puzzle.
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BlogPostViewSet
router = DefaultRouter()
router.register(r'blogposts', BlogPostViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
The DefaultRouter is generous. It gives you:
/api/blogposts/- GET (list), POST (create)/api/blogposts/1/- GET (retrieve), PUT (update), PATCH (partial_update), DELETE (destroy)
It also gives you a root API view, which is a nice touch. This is the DRF sweet spot. With about 15 lines of code, you have a full, production-ready REST API for a model.
The Pitfalls and Power Moves
This power comes with responsibility. Here’s what to watch for:
The Performance Killer (N+1 Queries): This is the big one. Your serializer might have a related field. When you list 100 posts, DRF will happily execute one query to get the 100 posts and then 100 more queries to get the author for each post. You must use
select_relatedandprefetch_relatedaggressively.class BlogPostViewSet(viewsets.ModelViewSet): def get_queryset(self): queryset = BlogPost.objects.all().select_related('author') # ... your other filtering logic ... return querysetOverwriting
createandupdate: Often, the default behavior isn’t enough. You need to assign the current user to theauthorfield on creation.class BlogPostViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): # The save method is passed the validated data plus whatever extra kwargs you provide serializer.save(author=self.request.user)Custom Validation: Your model might have validation rules that span multiple fields. The serializer’s
validatemethod is your friend.class BlogPostSerializer(serializers.ModelSerializer): # ... fields ... def validate(self, data): if data['is_published'] and not data.get('title'): raise serializers.ValidationError("Can't publish a post without a title.") return data
The beauty of DRF is that it gives you these simple, powerful defaults (ModelSerializer, ModelViewSet) to get you 90% of the way there with almost zero code. But it also provides a deep, granular escape hatch for every single one of those defaults when you need to get fancy. You start lazy and only write more code when you absolutely have to. That’s not just efficient; it’s elegant.