Skip to content

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:

  • PythonJSON

When your API receives data, the serializer:

  1. Validates — Is this data correct?
  2. Converts — Turn JSON into Python objects

When your API sends data, the serializer:

  1. Selects — Which fields should be included?
  2. 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:

{"id": "abc123", "title": "Buy milk", "completed": false}

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

  • ViewSets — Use serializers in API endpoints
  • Validation — Advanced validation techniques
  • Fields — Model field types