Nested serializers and relationships
owner has shown up as a bare integer — "owner": 1 — since part 3. That's technically correct (it's the ForeignKey's primary key) and often exactly what a client needs for writing, but it's not very useful for reading: nothing about 1 tells a client who that is without a second request.
StringRelatedField: the quick fix
# tasks/serializers.py
class TaskSerializer(serializers.ModelSerializer):
owner = serializers.StringRelatedField(read_only=True)
class Meta:
model = Task
fields = ["id", "title", "description", "completed", "due_date", "owner", "created_at"]{"id": 1, "title": "Write the API", "owner": "alice", "...": "..."}StringRelatedField renders the related object's __str__() instead of its ID — User's default __str__ is its username, so owner now reads "alice" instead of 1. read_only=True is required here: a string like "alice" isn't something DRF can reverse back into a specific User instance to save, so this field only ever works for output.
Nesting a full serializer
# accounts/serializers.py (or wherever the User serializer lives)
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]# tasks/serializers.py
from accounts.serializers import UserSerializer
class TaskSerializer(serializers.ModelSerializer):
owner = UserSerializer(read_only=True)
class Meta:
model = Task
fields = ["id", "title", "description", "completed", "due_date", "owner", "created_at"]{"id": 1, "title": "Write the API", "owner": {"id": 3, "username": "alice", "email": "alice@example.com"}, "...": "..."}Nesting UserSerializer directly gives a client the full related object in one response, instead of forcing a second request to /users/3/ just to show who owns a task. read_only=True is doing the same job as before: nested writes (accepting {"owner": {"username": "alice"}} on create and matching it to an existing user, or creating one) need explicit handling DRF doesn't provide automatically — safely out of scope until a project genuinely needs it.
Writing to a relationship: back to a plain ID
Creating or updating a task still needs to set the owner by ID, even though reading now shows a nested object — which is exactly why perform_create() from part 8 sets owner=self.request.user in the view rather than expecting it in the submitted JSON at all. For a relationship a client genuinely needs to set directly (assigning a task to a specific project, say), PrimaryKeyRelatedField is the explicit version of what ModelSerializer already infers automatically for a plain ForeignKey:
project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all())This accepts a plain ID on input (validating that it actually exists) while still just rendering as that ID on output — the middle ground between the bare-ID default and a fully nested, read-only UserSerializer above.
Nesting a nested serializer expecting it to also handle writes for free. owner = UserSerializer(read_only=True) only works because it's explicitly read-only — leaving that off and trying to POST a nested {"owner": {...}} object raises a NotImplementedError, since DRF has no automatic way to know whether that should update an existing user or create a new one.
The API returns real, structured data now. Next: performance and usability at scale — pagination, so /tasks/ doesn't return every row in one response forever.