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 withfields.fields = '__all__'is a footgun: the day someone adds apassword_hashoris_staffcolumn, your API leaks it. List fields deliberately.
Example
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 orderWhen 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.
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.
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 postOverride to_representation()
Transforms the author field in the output to a full name string without adding a separate SerializerMethodField.
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