When you first start building APIs with Django REST Framework you learn about serializers. The first thing you learn is that they validate incoming data. You get a POST request with some JSON and the serializer tells you if the required fields are there and if the data types are correct. This is useful so most developers stop there. They think of serializers as a validation layer.
This is a mistake. It is like using a powerful tool for only its most basic function. The real power of serializers is not in validating incoming data but in representing outgoing data. They are a declarative way to translate your complex Django models into the exact JSON structure your frontend needs. Thinking of them this way changes how you write your API views and makes your code much cleaner.
Without a clear understanding of serializers API views can get messy. You fetch some data from the database and then start building dictionaries by hand to create the JSON response.
Imagine you have a simple model for a blog post.
# models.py
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
published_at = models.DateTimeField(auto_now_add=True)
In a view you might write code like this to return a list of posts.
# views.py (the messy way)
from django.http import JsonResponse
from .models import Post
def post_list(request):
posts = Post.objects.all().select_related('author')
data = []
for post in posts:
data.append({
'title': post.title,
'author_username': post.author.username,
'published_date': post.published_at.strftime('%Y-%m-%d')
})
return JsonResponse({'posts': data})
This works for a simple case. But what happens when you need to add more fields? Or change the date format? Or add a URL to the post? The view gets bigger and more complex. You are mixing data fetching with data presentation. This makes the code hard to read and harder to change.
Serializers let you separate the 'what' from the 'how'. You define what the JSON representation of a Post
should look like in one place a serializer class. The view then just uses this serializer to do the work.
Let’s create a serializer for our Post
model.
# serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'published_at']
This ModelSerializer
does a lot automatically. It inspects the Post
model and creates fields that map to the model’s fields. Now we can rewrite our view to be much simpler.
# views.py (the clean way)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
@api_view(['GET'])
def post_list_clean(request):
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data)
Look at how clean that view is. Its only job is to get the queryset. The serializer handles the entire process of turning that queryset into JSON. If you need to change the output you change the serializer not the view.
The real power becomes clear when you have relationships. In our first messy example we manually pulled the author’s username using post.author.username
. A serializer can handle this and much more.
By default the serializer will represent the author
foreign key with its primary key. That is often not what you want. You can change this behavior easily.
For example to show the author’s username you can use StringRelatedField
.
# serializers.py
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'published_at']
Now the author
field in the JSON will be the string representation of the User model which is its username. This is much better. You could also create another serializer for the User
model and nest it to include more author details.
What if you need a field in your JSON that does not exist directly on the model? This is a very common requirement. You might want to include a URL to the object or a calculated value.
This is where SerializerMethodField
is incredibly useful.
Let’s say we want to include a word count for the post content and also an absolute URL for the API endpoint of that specific post.
# serializers.py
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
word_count = serializers.SerializerMethodField()
absolute_url = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'author', 'word_count', 'absolute_url', 'published_at']
def get_word_count(self, obj):
return len(obj.content.split())
def get_absolute_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(f'/api/posts/{obj.id}/')
Now our JSON output will have word_count
and absolute_url
fields. The logic for creating these fields is neatly contained within the serializer. Notice we also passed the request object into the serializer’s context from the view so we could build a full URL. The view is still simple.
# views.py
@api_view(['GET'])
def post_list_clean(request):
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True, context={'request': request})
return Response(serializer.data)
This pattern is powerful. It keeps your views focused on handling requests and your serializers focused on shaping the response. It is the single responsibility principle applied to your API code.
When you think of serializers as tools for representation you start to see them as the most important part of your API. They define your API’s contract with the outside world. Validation is just a small part of that. The bigger job is building a clean and consistent data structure for your users.
Now think about an API you've worked on. What’s the most complex JSON structure you've had to build in an API?
— Rishi Banerjee
September 2025