DRF Serializers

Turn models into JSON and back with ModelSerializer — with validation, nested data and computed fields done right.

Django REST Framework (DRF) is the de facto way to build APIs in Django. Its cornerstone is the serializer: it converts model instances to JSON (serialization) and validates incoming JSON back into model data (deserialization). Think of it as a Form for APIs.

ModelSerializer — the 90% case

from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'price']
        read_only_fields = ['id']

That single class handles both directions. But real APIs need more, and DRF has a clean hook for each:

  • SerializerMethodField — a read-only computed value (get_<field>()).
  • validate_<field>() — validate one field; validate() — cross-field rules.
  • Nested serializers — embed related objects; override create()/update() for writable nesting.
Always be explicit with fields. fields = '__all__' is a footgun: the day someone adds a password_hash or is_staff column, your API leaks it. List fields deliberately.

Example

Example · python
from rest_framework import serializers
from django.db import transaction

from .models import Order, OrderLine, Product


class OrderLineSerializer(serializers.ModelSerializer):
    product_name = serializers.CharField(source='product.name', read_only=True)

    class Meta:
        model = OrderLine
        fields = ['product', 'product_name', 'quantity', 'unit_price']
        read_only_fields = ['unit_price']


class OrderSerializer(serializers.ModelSerializer):
    lines = OrderLineSerializer(many=True)
    total = serializers.SerializerMethodField()
    customer_email = serializers.EmailField(source='customer.email', read_only=True)

    class Meta:
        model = Order
        fields = ['id', 'customer', 'customer_email', 'status', 'lines', 'total']
        read_only_fields = ['id', 'status']

    def get_total(self, obj):
        return sum(line.quantity * line.unit_price for line in obj.lines.all())

    def validate_lines(self, value):
        if not value:
            raise serializers.ValidationError('An order needs at least one line.')
        return value

    def validate(self, attrs):
        # Cross-field / stock check across the whole payload.
        for line in attrs['lines']:
            if line['quantity'] > line['product'].stock:
                raise serializers.ValidationError(
                    f"Only {line['product'].stock} of {line['product']} in stock."
                )
        return attrs

    @transaction.atomic
    def create(self, validated_data):
        lines = validated_data.pop('lines')
        order = Order.objects.create(**validated_data)
        OrderLine.objects.bulk_create([
            OrderLine(
                order=order,
                unit_price=line['product'].price,   # snapshot price at purchase
                **line,
            )
            for line in lines
        ])
        return order

When to use it

  • A developer uses validate_email in a serializer to check that a submitted email is not already taken in the database.
  • A team uses a writable nested serializer to create a post and its tags in a single API request.
  • A developer overrides to_representation() to rename or hide fields in the API output without changing the model.

More examples

Field-level serializer validation

Validates email uniqueness in the serializer and hashes the password via create_user() on save.

Example · python
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["username", "email", "password"]
        extra_kwargs = {"password": {"write_only": True}}

    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            raise serializers.ValidationError("Email already in use.")
        return value

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

Writable nested serializer

Accepts nested tag data in a POST request and creates or reuses Tag objects during post creation.

Example · python
class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ["name"]

class PostSerializer(serializers.ModelSerializer):
    tags = TagSerializer(many=True)

    class Meta:
        model = Post
        fields = ["title", "body", "tags"]

    def create(self, validated_data):
        tags_data = validated_data.pop("tags", [])
        post = Post.objects.create(**validated_data)
        for tag in tags_data:
            tag_obj, _ = Tag.objects.get_or_create(**tag)
            post.tags.add(tag_obj)
        return post

Override to_representation()

Transforms the author field in the output to a full name string without adding a separate SerializerMethodField.

Example · python
class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ["id", "title", "body", "author"]

    def to_representation(self, instance):
        rep = super().to_representation(instance)
        rep["author"] = instance.author.get_full_name() or instance.author.username
        return rep

Discussion

  • Be the first to comment on this lesson.