Skip to content

Serializers

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types.

They also provide deserialization, validating parsed data before saving it back to complex types.

Basic Serializer

A basic serializer is very similar to a Django Form.

python
from rest_framework import serializers

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

A plain Serializer only describes fields. It will validate incoming data and render output, but calling .save() on one raises NotImplementedError until you write create() and update() yourself:

python
class CommentSerializer(serializers.Serializer):
    # ... fields as above

    def create(self, validated_data):
        return Comment.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.email = validated_data.get('email', instance.email)
        instance.content = validated_data.get('content', instance.content)
        instance.save()
        return instance

Writing those two methods for every model is exactly what ModelSerializer saves you from.

Using Serializers

To serialize a single object, pass the instance to the serializer and access .data:

python
comment = Comment.objects.first()
serializer = CommentSerializer(comment)
print(serializer.data)
# {'email': '[email protected]', 'content': 'Hello world', ...}

To serialize a queryset or a list of objects, pass many=True:

python
comments = Comment.objects.all()
serializer = CommentSerializer(comments, many=True)
print(serializer.data)
# [{'email': '...'}, {'email': '...'}]

When deserializing data (like processing an incoming POST request), you must call is_valid() before accessing the .validated_data dictionary or calling .save():

python
# When creating a new object (like in a POST request):
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
    serializer.save()

# When updating an existing object (like in a PATCH request), pass the instance
# and use partial=True so missing fields don't cause validation errors:
serializer = CommentSerializer(comment, data=request.data, partial=True)
if serializer.is_valid():
    # .validated_data contains only the parsed, valid fields sent in the request
    print(serializer.validated_data)
    serializer.save()

ModelSerializers

Often, your serializers map closely to Django models. The ModelSerializer class provides a shortcut that lets you automatically create a Serializer class with fields that correspond to the Model fields.

python
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=100)
    body = models.TextField()
python
from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'body']
        # Use fields = '__all__' to include all fields

__all__

When you want a ModelSerializer to automatically serialize every standard field on your model, you can set fields = '__all__' instead of listing each field name individually.

However, __all__ is not a magical wildcard over your database columns. Instead, it instructs DRF to generate a specific, predefined set of fields.

What __all__ includes:

  • The primary key (e.g., id), automatically marked as read-only (unless you defined a custom, writable primary key, like a CharField).
  • Every concrete model field, including non-editable fields like auto_now_add timestamps (which also become read-only).
  • Every forward relation (ForeignKey, OneToOneField, ManyToManyField), represented as a PrimaryKeyRelatedField (or a HyperlinkedRelatedField if using HyperlinkedModelSerializer).
  • Explicitly declared fields, meaning any custom field you define directly on the serializer class.

What __all__ NEVER includes:

  • Reverse relations (e.g., article.comment_set).
  • Model properties (e.g., @property methods).
  • Generic relationships (GenericForeignKey).

To expose reverse relations, properties, or generic foreign keys, you must declare them explicitly on the serializer (often using a SerializerMethodField). Because __all__ automatically picks up explicitly declared fields, they will be included in the output alongside the generated model fields.

WARNING

Using __all__ automatically exposes newly added model columns to your API. This can silently leak sensitive or private data in public API responses. While fine for quick prototyping, always use an explicit fields list for production APIs.

read_only_fields

ModelSerializer generates its fields from the model and Meta gives you two ways to adjust them without redeclaring a field by hand.

read_only_fields marks generated fields as output only, which is the usual way to protect values the server owns:

python
class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'body', 'author', 'created']
        read_only_fields = ['author', 'created']

extra_kwargs

extra_kwargs passes arbitrary arguments to a generated field, covering everything read_only_fields cannot express:

python
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'password']
        extra_kwargs = {
            'password': {'write_only': True, 'min_length': 8}
        }

NOTE

Both options only apply to fields DRF generates for you. If you declare a field explicitly on the serializer class, pass its arguments there instead - extra_kwargs is ignored for declared fields.

HyperlinkedModelSerializer

The HyperlinkedModelSerializer is similar to ModelSerializer, but it represents relationships using hyperlinks instead of primary keys.

This means that instead of returning a nested object or an integer ID, it returns a URL pointing to the detailed view of the related object (or the object itself).

By default, the serializer will include a url field for the object itself instead of an id field. Additionally, any relationships on the model will also be represented as URLs pointing to the related object's detail view.

python
from django.db import models
from django.contrib.auth.models import User

class Account(models.Model):
    account_name = models.CharField(max_length=100)
    users = models.ManyToManyField(User)
    created = models.DateTimeField(auto_now_add=True)
python
from rest_framework import serializers
from .models import Account

class AccountSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Account
        # 'url' links to this Account instance.
        # 'users' will be a list of URLs linking to each User.
        fields = ['url', 'account_name', 'users', 'created']
json
{
    "url": "http://api.example.com/accounts/1/",
    "account_name": "Acme Corp",
    "users": [
        "http://api.example.com/users/4/",
        "http://api.example.com/users/9/"
    ],
    "created": "2026-08-10T12:00:00Z"
}

Request Context

For DRF to generate absolute URLs (like http://api.example.com/...), the serializer requires the current HTTP request object in its context.

When using DRF's generic views (like GenericAPIView, ListCreateAPIView, or ViewSet), the request context is passed to the serializer automatically because these views build the serializer for you via get_serializer(). However, if you are using a base APIView (or @api_view), you construct the serializer yourself. In this case, you must pass the context yourself, otherwise DRF will raise an error:

python
# Doing this manually requires passing context={'request': request}
serializer = AccountSerializer(account, context={'request': request})
print(serializer.data)

How URLs are resolved

For the url field to work, DRF needs to know which view routes to the object. By default, it expects a view named '{model_name}-detail' to exist in your URL configuration (e.g., account-detail or user-detail).

If you are using a custom view name or a different lookup field (like a slug instead of an ID), you can override the defaults using extra_kwargs:

python
class AccountSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Account
        fields = ['url', 'account_name', 'users', 'created']
        extra_kwargs = {
            'url': {
                'view_name': 'custom-account-detail',
                'lookup_field': 'account_name' # Uses account_name in the URL instead of ID
            }
        }

NOTE

lookup_field only tells the serializer how to build the URL. The view or router must be configured with the same lookup_field, otherwise reversing the URL fails with NoReverseMatch. A slug is usually a better choice than a name like account_name, which can change and may contain characters that are not URL safe.