Tasks Serializers - Data Validation

File: api/nextango/apps/tasks/serializers.py Purpose: DRF serializers for task data validation and transformation Framework: Django REST Framework (ModelSerializer)

Overview

Tasks serializers provide data validation, transformation, and nested representation for Task models. Uses DRF's ModelSerializer pattern with read-only fields for related object names.

Architecture:

HTTP Request (JSON)

    ├─ Deserialization & Validation


TaskSerializer

    ├─ Validate field types and constraints
    ├─ Read-only fields (auto-populated)
    ├─ Related object name resolution


Clean Python Dict → Create/Update Model

TaskSerializer

Purpose: Serialize and validate Task model data with related object names

Source Code:

# From serializers.py:4-14

class TaskSerializer(serializers.ModelSerializer):
    assigned_to_name = serializers.CharField(source='assigned_to.name', read_only=True)
    completed_by_name = serializers.CharField(source='completed_by.name', read_only=True)
    store_name = serializers.CharField(source='store.name', read_only=True)

    class Meta:
        model = Task
        fields = ['id', 'task_id', 'title', 'description', 'assigned_to', 'assigned_to_name',
                 'store', 'store_name', 'priority', 'status', 'due_date', 'completed_by',
                 'completed_by_name', 'completed_at', 'created_at']
        read_only_fields = ['task_id', 'completed_at', 'created_at']

Fields

Read-Only Fields:

  • id (UUIDField) - Auto-generated UUID primary key
  • task_id (CharField) - Business identifier (auto-generated)
  • completed_at (DateTimeField) - Auto-set when task completed
  • created_at (DateTimeField) - Auto-set on creation
  • assigned_to_name (CharField) - Denormalized employee name
  • completed_by_name (CharField) - Denormalized completer name
  • store_name (CharField) - Denormalized store name

Write Fields:

  • title (CharField, required, max_length=255) - Task title
  • description (TextField, optional) - Task details
  • assigned_to (UUIDField, optional) - Employee UUID
  • store (UUIDField, required) - Store UUID
  • priority (ChoiceField, required) - 'low', 'medium', or 'high'
  • status (ChoiceField, default='pending') - 'pending', 'in_progress', or 'completed'
  • due_date (DateField, optional) - Task deadline
  • completed_by (UUIDField, optional) - Completer UUID

Request/Response Examples

Create Task Request:

POST /api/tasks/

{
  "title": "Restock dairy section",
  "description": "Restock milk, yogurt, and cheese products before 3 PM",
  "assigned_to": "a1b2c3d4-...",
  "store": "store-uuid-...",
  "priority": "high",
  "status": "pending",
  "due_date": "2025-11-20"
}

Create Task Response:

HTTP 201 Created

{
  "id": "task-uuid-...",
  "task_id": "task_12345",
  "title": "Restock dairy section",
  "description": "Restock milk, yogurt, and cheese products before 3 PM",
  "assigned_to": "a1b2c3d4-...",
  "assigned_to_name": "John Doe",
  "store": "store-uuid-...",
  "store_name": "Downtown Location",
  "priority": "high",
  "status": "pending",
  "due_date": "2025-11-20",
  "completed_by": null,
  "completed_by_name": null,
  "completed_at": null,
  "created_at": "2025-11-18T10:30:00Z"
}

Update Task Request (Mark In Progress):

PATCH /api/tasks/task_12345/

{
  "status": "in_progress"
}

List Tasks Response:

GET /api/tasks/

[
  {
    "id": "task-uuid-1",
    "task_id": "task_12345",
    "title": "Restock dairy section",
    "assigned_to_name": "John Doe",
    "store_name": "Downtown Location",
    "priority": "high",
    "status": "in_progress",
    "due_date": "2025-11-20",
    ...
  },
  {
    "id": "task-uuid-2",
    "task_id": "task_67890",
    "title": "Clean POS terminals",
    "assigned_to_name": null,
    "store_name": "Downtown Location",
    "priority": "medium",
    "status": "pending",
    "due_date": null,
    ...
  }
]

Pattern: Denormalized Read-Only Fields

Purpose: Include human-readable names without additional API calls

# From serializers.py:5-7
assigned_to_name = serializers.CharField(source='assigned_to.name', read_only=True)
completed_by_name = serializers.CharField(source='completed_by.name', read_only=True)
store_name = serializers.CharField(source='store.name', read_only=True)

Behavior:

  • Read-only: Cannot be set via API, auto-populated from relationships
  • Source traversal: source='assigned_to.name' follows FK → gets name field
  • Null handling: Returns null if relationship is null (assigned_to, completed_by)
  • Required relationship: store_name always populated (store is required)

Benefits:

  1. Reduced API calls: Get names in single response (no need to fetch /api/users/{id}/)
  2. Frontend convenience: Display "John Doe" instead of UUID
  3. Backward compatibility: UUIDs still available for updates

SQL Impact:

-- Without names: Single query
SELECT * FROM tasks_task WHERE task_id = 'task_12345';

-- With names: Requires JOINs (handled by ViewSet queryset)
SELECT
    tasks_task.*,
    users_user_assigned.name AS assigned_to_name,
    users_user_completed.name AS completed_by_name,
    stores_store.name AS store_name
FROM tasks_task
LEFT JOIN users_user users_user_assigned ON tasks_task.assigned_to_id = users_user_assigned.id
LEFT JOIN users_user users_user_completed ON tasks_task.completed_by_id = users_user_completed.id
INNER JOIN stores_store ON tasks_task.store_id = stores_store.id;

Important: ViewSet must use select_related() or prefetch_related() to avoid N+1 queries.


Field Validation

Priority Validation

Allowed Values:

  • low
  • medium
  • high

Validation:

# From models.py:5
PRIORITY_CHOICES = [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')]

Invalid Request:

{
  "priority": "critical"
}

Error Response:

HTTP 400 Bad Request

{
  "priority": ["\"critical\" is not a valid choice."]
}

Status Validation

Allowed Values:

  • pending (default)
  • in_progress
  • completed

Validation:

# From models.py:6
TASK_STATUS_CHOICES = [('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed')]

Required Fields

Always Required:

  • title - Cannot be blank
  • store - Must reference valid Store UUID
  • priority - Must be valid choice

Optional:

  • description
  • assigned_to
  • due_date
  • completed_by

DeliveryScheduleSerializer (Commented Out)

Source Code:

# From serializers.py:16-24 (commented out)

# class DeliveryScheduleSerializer(serializers.ModelSerializer):
#     vendor_name = serializers.CharField(source='vendor.name', read_only=True)
#     store_name = serializers.CharField(source='store.name', read_only=True)

#     class Meta:
#         model = DeliverySchedule
#         fields = ['id', 'delivery_id', 'vendor', 'vendor_name', 'store', 'store_name',
#                  'scheduled_date', 'scheduled_time', 'status', 'actual_arrival']
#         read_only_fields = ['delivery_id']

Status:

  • Model exists in models.py
  • Serializer implementation commented out
  • ViewSet commented out in views.py
  • URL routing commented out in urls.py

Likely Reason: Feature pending activation or behind feature flag

Note: The commented code references vendor.name but the actual DeliverySchedule model has vendor_name as a CharField (not a FK). This would cause an error if uncommented.

Expected Implementation:

class DeliveryScheduleSerializer(serializers.ModelSerializer):
    store_name = serializers.CharField(source='store.name', read_only=True)

    class Meta:
        model = DeliverySchedule
        fields = ['id', 'delivery_id', 'vendor_name', 'vendor_reference', 'store',
                 'store_name', 'scheduled_date', 'scheduled_time', 'status', 'actual_arrival']
        read_only_fields = ['delivery_id']

Integration Points

Used By

TaskViewSet:

# From views.py:9-12
class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    lookup_field = 'task_id'

Standard CRUD Operations:

  • POST /api/tasks/ - Create task (TaskSerializer validates input)
  • GET /api/tasks/ - List tasks (TaskSerializer formats output)
  • GET /api/tasks/{task_id}/ - Retrieve task (uses TaskService, not serializer)
  • PUT /api/tasks/{task_id}/ - Update task (TaskSerializer validates)
  • PATCH /api/tasks/{task_id}/ - Partial update (TaskSerializer validates)
  • DELETE /api/tasks/{task_id}/ - Delete task

Performance Considerations

Queryset Optimization Required

Problem: Serializer accesses assigned_to.name, completed_by.name, store.name

Without Optimization (N+1 Queries):

# ViewSet queryset
queryset = Task.objects.all()

# Causes 1 + N queries when serializing list
# Query 1: SELECT * FROM tasks_task
# Query 2-N: SELECT name FROM users_user WHERE id = ...
# Query N+1: SELECT name FROM stores_store WHERE id = ...

With Optimization (Single Query):

# Recommended ViewSet queryset
queryset = Task.objects.select_related(
    'assigned_to', 'completed_by', 'store'
).all()

# Single query with JOINs

Where This Matters:

  • List endpoint (GET /api/tasks/) - Critical for performance
  • Retrieve endpoint (GET /api/tasks/{task_id}/) - TaskService handles optimization

Usage Examples

Create Task

from nextango.apps.tasks.serializers import TaskSerializer
from nextango.apps.stores.models import Store
from nextango.apps.users.models import User

# Prepare data
task_data = {
    'title': 'Clean refrigerators',
    'description': 'Deep clean all refrigerators in dairy section',
    'assigned_to': User.objects.get(employee_id='EMP-001').id,
    'store': Store.objects.get(store_code='DT-01').id,
    'priority': 'medium',
    'status': 'pending',
    'due_date': '2025-11-21'
}

# Validate and save
serializer = TaskSerializer(data=task_data)
if serializer.is_valid():
    task = serializer.save()
    print(f"Created task: {task.task_id}")
    print(f"Assigned to: {serializer.data['assigned_to_name']}")
else:
    print(f"Validation errors: {serializer.errors}")

Update Task Status

from nextango.apps.tasks.models import Task
from nextango.apps.tasks.serializers import TaskSerializer

# Get existing task
task = Task.objects.get(task_id='task_12345')

# Update status
serializer = TaskSerializer(task, data={'status': 'in_progress'}, partial=True)
if serializer.is_valid():
    serializer.save()
    print(f"Task updated: {serializer.data}")
else:
    print(f"Validation errors: {serializer.errors}")

Complete Task

from django.utils import timezone
from nextango.apps.tasks.models import Task
from nextango.apps.tasks.serializers import TaskSerializer

# Get task and completer
task = Task.objects.get(task_id='task_12345')
completer = User.objects.get(employee_id='EMP-002')

# Update with completion data
completion_data = {
    'status': 'completed',
    'completed_by': completer.id,
    'completed_at': timezone.now()
}

serializer = TaskSerializer(task, data=completion_data, partial=True)
if serializer.is_valid():
    serializer.save()
    print(f"Task completed by: {serializer.data['completed_by_name']}")

Validation Patterns

Foreign Key Validation

Pattern: Serializer validates FK UUIDs reference existing objects

# Valid request
{
  "assigned_to": "valid-user-uuid",
  "store": "valid-store-uuid"
}

# Invalid FK - nonexistent UUID
{
  "assigned_to": "nonexistent-uuid"
}

# Error response
{
  "assigned_to": ["Invalid pk \"nonexistent-uuid\" - object does not exist."]
}

Choice Field Validation

Pattern: Only predefined choices allowed

# Valid priority
{"priority": "high"}

# Invalid priority
{"priority": "urgent"}

# Error
{"priority": ["\"urgent\" is not a valid choice."]}

Date Validation

Pattern: ISO 8601 date format

# Valid date
{"due_date": "2025-11-20"}

# Invalid date format
{"due_date": "11/20/2025"}

# Error
{"due_date": ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD."]}

Was this page helpful?