Serializers¶
Convert data between Python objects and JSON, and validate incoming requests.
What is a Serializer?¶
A Serializer is like a translator between two languages:
- Python ↔ JSON
When your API receives data, the serializer:
- Validates — Is this data correct?
- Converts — Turn JSON into Python objects
When your API sends data, the serializer:
- Selects — Which fields should be included?
- Converts — Turn Python objects into JSON
SERIALIZER
│
┌──────────────┐ │ ┌──────────────┐
│ JSON │ ───────►│────────►│ Python │
│ Request │ │ │ Object │
│ │ │ │ │
│ {"title": │ Input │ Output │ task.title │
│ "Buy milk"} │ │ │ = "Buy milk" │
└──────────────┘ │ └──────────────┘
│
Basic Usage¶
Creating a Serializer¶
from aksara.api import ModelSerializer
from myapp.models import Task
class TaskSerializer(ModelSerializer):
"""
Converts Task objects to/from JSON.
"""
class Meta:
model = Task
fields = ["id", "title", "description", "completed", "created_at"]
Using a Serializer¶
# Serialize: Python → JSON
task = await Task.objects.get(id=task_id)
serializer = TaskSerializer(task)
json_data = serializer.data
# {"id": "abc123", "title": "Buy milk", "completed": false, ...}
# Deserialize: JSON → Python
data = {"title": "Buy eggs", "description": "Get a dozen"}
serializer = TaskSerializer(data=data)
serializer.is_valid(raise_exception=True) # Validates
task = await serializer.save() # Creates the object
Specifying Fields¶
Include Specific Fields¶
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "completed"] # Only these fields
Result:
Include All Fields¶
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = "__all__" # Every field on the model
Exclude Fields¶
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
exclude = ["internal_notes", "deleted_at"] # Everything except these
Field Access Control¶
Read-Only Fields¶
What it means: Users can see these fields but can't change them.
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "description", "completed", "created_at"]
read_only_fields = ["id", "created_at"] # Can't be set by user
Use case: IDs, timestamps, calculated values.
Write-Only Fields¶
What it means: Users can send these fields but won't see them in responses.
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ["id", "email", "password", "name"]
write_only_fields = ["password"] # Hidden in responses
Use case: Passwords, secrets, internal codes.
Validation¶
Automatic Validation¶
Serializers automatically validate:
| Check | What It Does |
|---|---|
| Required fields | Ensures all required fields are present |
| Data types | Ensures correct types (string, number, etc.) |
| Max length | Ensures strings don't exceed limits |
| Valid values | Ensures enum values are valid |
# This will fail validation:
data = {"title": ""} # Title is required and can't be empty
serializer = TaskSerializer(data=data)
serializer.is_valid() # Returns False
print(serializer.errors) # {"title": ["This field is required."]}
Field-Level Validation¶
Add custom validation for a specific field.
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "description"]
def validate_title(self, value):
"""
Custom validation for the title field.
This runs when someone tries to set the title.
"""
# Check minimum length
if len(value) < 3:
raise ValidationError("Title must be at least 3 characters")
# Check for banned words
banned_words = ["spam", "test123"]
if any(word in value.lower() for word in banned_words):
raise ValidationError("Title contains banned words")
return value # Return the value (possibly modified)
Object-Level Validation¶
Validate multiple fields together.
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "due_date", "reminder_date"]
def validate(self, data):
"""
Validate the entire object.
Use this when validation depends on multiple fields.
"""
due_date = data.get("due_date")
reminder_date = data.get("reminder_date")
if reminder_date and due_date and reminder_date > due_date:
raise ValidationError({
"reminder_date": "Reminder must be before the due date"
})
return data
Custom Fields¶
Computed Fields¶
Add fields that don't exist on the model.
from aksara.api import ModelSerializer, SerializerMethodField
class TaskSerializer(ModelSerializer):
# Field computed from a method
summary = SerializerMethodField()
is_overdue = SerializerMethodField()
class Meta:
model = Task
fields = ["id", "title", "description", "summary", "is_overdue", "due_date"]
def get_summary(self, obj):
"""
Create a short summary from the description.
Method name must be `get_<field_name>`.
"""
if not obj.description:
return ""
return obj.description[:100] + "..." if len(obj.description) > 100 else obj.description
def get_is_overdue(self, obj):
"""Check if task is past due date."""
if not obj.due_date:
return False
return obj.due_date < datetime.now() and not obj.completed
Result:
{
"id": "abc123",
"title": "Buy milk",
"description": "Get 2% milk from the store on Main Street...",
"summary": "Get 2% milk from the store on Main Street...",
"is_overdue": true,
"due_date": "2024-01-01T10:00:00Z"
}
Renamed Fields¶
Expose a field with a different name.
from aksara.api import ModelSerializer, Field
class TaskSerializer(ModelSerializer):
# Expose "created_at" as "createdAt" for JavaScript
createdAt = Field(source="created_at", read_only=True)
class Meta:
model = Task
fields = ["id", "title", "createdAt"]
Nested Serializers¶
Include related objects in your response.
Basic Nesting¶
class AuthorSerializer(ModelSerializer):
class Meta:
model = User
fields = ["id", "name", "email"]
class PostSerializer(ModelSerializer):
# Nest the author object
author = AuthorSerializer(read_only=True)
class Meta:
model = Post
fields = ["id", "title", "content", "author"]
Result:
{
"id": "post-123",
"title": "Hello World",
"content": "This is my first post...",
"author": {
"id": "user-456",
"name": "Alice",
"email": "alice@example.com"
}
}
Many Nested Objects¶
For lists of related objects (like tags on a post).
class TagSerializer(ModelSerializer):
class Meta:
model = Tag
fields = ["id", "name"]
class PostSerializer(ModelSerializer):
# many=True for lists
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ["id", "title", "tags"]
Result:
{
"id": "post-123",
"title": "Python Tips",
"tags": [
{"id": "tag-1", "name": "python"},
{"id": "tag-2", "name": "programming"}
]
}
Different Serializers for Different Actions¶
Use simple serializers for lists, detailed for single items.
# Simple serializer for list view (less data = faster)
class TaskListSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "completed"]
# Detailed serializer for detail view (all the info)
class TaskDetailSerializer(ModelSerializer):
author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Task
fields = ["id", "title", "description", "completed",
"author", "tags", "created_at", "updated_at"]
Use in ViewSet:
class TaskViewSet(ModelViewSet):
model = Task
serializer_class = TaskDetailSerializer # For single item
list_serializer_class = TaskListSerializer # For list view
Context¶
Pass extra information to the serializer.
# In ViewSet
class TaskViewSet(ModelViewSet):
model = Task
def get_serializer_context(self):
"""Add the current user to serializer context."""
return {
"user": self.request.user,
"request": self.request,
}
# In Serializer
class TaskSerializer(ModelSerializer):
is_owner = SerializerMethodField()
class Meta:
model = Task
fields = ["id", "title", "is_owner"]
def get_is_owner(self, obj):
"""Check if current user owns this task."""
user = self.context.get("user")
return user and obj.user_id == user.id
Creating and Updating¶
Creating Objects¶
# Data from request
data = {"title": "New Task", "description": "Do something"}
# Validate
serializer = TaskSerializer(data=data)
serializer.is_valid(raise_exception=True)
# Create
task = await serializer.save()
Updating Objects¶
# Get existing object
task = await Task.objects.get(id=task_id)
# Partial update (PATCH) - only provided fields
data = {"completed": True}
serializer = TaskSerializer(task, data=data, partial=True)
serializer.is_valid(raise_exception=True)
updated_task = await serializer.save()
# Full update (PUT) - all fields required
data = {"title": "Updated Task", "description": "New description", "completed": True}
serializer = TaskSerializer(task, data=data)
serializer.is_valid(raise_exception=True)
updated_task = await serializer.save()
Custom Create/Update Logic¶
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ["id", "title", "description"]
async def create(self, validated_data):
"""Custom create logic."""
# Add extra data
validated_data["user_id"] = self.context["user"].id
# Create the object
return await Task.objects.create(**validated_data)
async def update(self, instance, validated_data):
"""Custom update logic."""
# Track what changed
old_title = instance.title
# Update fields
for key, value in validated_data.items():
setattr(instance, key, value)
await instance.save()
# Log if title changed
if old_title != instance.title:
print(f"Task renamed: {old_title} → {instance.title}")
return instance
Quick Reference¶
Meta Options¶
| Option | What It Does | Example |
|---|---|---|
model |
The model to serialize | model = Task |
fields |
Which fields to include | ["id", "title"] or "__all__" |
exclude |
Which fields to exclude | ["internal_notes"] |
read_only_fields |
Can read, can't write | ["id", "created_at"] |
write_only_fields |
Can write, can't read | ["password"] |
Field Types¶
| Type | Use Case |
|---|---|
Field() |
Basic field with options |
SerializerMethodField() |
Computed from a method |
PrimaryKeyRelatedField() |
Return ID of related object |
StringRelatedField() |
Return str() of related object |
NestedSerializer() |
Full nested object |
Validation Methods¶
| Method | When It Runs |
|---|---|
validate_<field>(value) |
For one field |
validate(data) |
For entire object |
Complete Example¶
from datetime import datetime
from aksara.api import ModelSerializer, SerializerMethodField
from aksara.api.validators import ValidationError
from myapp.models import Task, User, Tag
class UserBriefSerializer(ModelSerializer):
"""Minimal user info for nesting."""
class Meta:
model = User
fields = ["id", "name"]
class TagSerializer(ModelSerializer):
class Meta:
model = Tag
fields = ["id", "name"]
class TaskSerializer(ModelSerializer):
"""
Full task serializer with related objects and computed fields.
"""
# Nested objects
assigned_to = UserBriefSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
# Computed fields
is_overdue = SerializerMethodField()
summary = SerializerMethodField()
class Meta:
model = Task
fields = [
"id", "title", "description", "summary",
"completed", "is_overdue",
"due_date", "assigned_to", "tags",
"created_at", "updated_at"
]
read_only_fields = ["id", "created_at", "updated_at"]
def get_is_overdue(self, obj):
if not obj.due_date or obj.completed:
return False
return obj.due_date < datetime.now()
def get_summary(self, obj):
if not obj.description:
return ""
return obj.description[:100] + "..." if len(obj.description) > 100 else obj.description
def validate_title(self, value):
if len(value) < 3:
raise ValidationError("Title must be at least 3 characters")
return value
def validate(self, data):
due_date = data.get("due_date")
if due_date and due_date < datetime.now():
raise ValidationError({"due_date": "Due date cannot be in the past"})
return data
Related Documentation¶
- ViewSets — Use serializers in API endpoints
- Validation — Advanced validation techniques
- Fields — Model field types