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 keytask_id(CharField) - Business identifier (auto-generated)completed_at(DateTimeField) - Auto-set when task completedcreated_at(DateTimeField) - Auto-set on creationassigned_to_name(CharField) - Denormalized employee namecompleted_by_name(CharField) - Denormalized completer namestore_name(CharField) - Denormalized store name
Write Fields:
title(CharField, required, max_length=255) - Task titledescription(TextField, optional) - Task detailsassigned_to(UUIDField, optional) - Employee UUIDstore(UUIDField, required) - Store UUIDpriority(ChoiceField, required) - 'low', 'medium', or 'high'status(ChoiceField, default='pending') - 'pending', 'in_progress', or 'completed'due_date(DateField, optional) - Task deadlinecompleted_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,
...
}
]
Related Object Name Fields
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 → getsnamefield - Null handling: Returns
nullif relationship is null (assigned_to, completed_by) - Required relationship:
store_namealways populated (store is required)
Benefits:
- Reduced API calls: Get names in single response (no need to fetch /api/users/{id}/)
- Frontend convenience: Display "John Doe" instead of UUID
- 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:
lowmediumhigh
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_progresscompleted
Validation:
# From models.py:6
TASK_STATUS_CHOICES = [('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed')]
Required Fields
Always Required:
title- Cannot be blankstore- Must reference valid Store UUIDpriority- Must be valid choice
Optional:
descriptionassigned_todue_datecompleted_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."]}
Related Documentation
- models.md - Task and DeliverySchedule models
- views.md - TaskViewSet API endpoints
- services.md - TaskService business logic
- Users Serializers - User model serialization
- Stores Serializers - Store model serialization