Relations
Relational fields are used to represent model relationships. They can be applied to ForeignKey, ManyToManyField and OneToOneField relationships.
Shared Models
For the following examples, assume these Album and Track models are defined:
from django.db import models
from django.utils.text import slugify
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def __str__(self):
return self.titlePrimaryKeyRelatedField
PrimaryKeyRelatedField may be used to represent the target of the relationship using its primary key.
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']{
"album_name": "The Dark Side of the Moon",
"artist": "Pink Floyd",
"tracks": [
89,
90,
91
]
}NOTE
The name of the variable you assign a relational field to in your serializer must exactly match the name of the relationship field on your Django model. If you want to use a different name for the output field, you must pass the source argument (e.g., song_list = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='tracks')).
StringRelatedField
StringRelatedField may be used to represent the target of the relationship using its __str__ method.
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']{
"album_name": "The Dark Side of the Moon",
"artist": "Pink Floyd",
"tracks": [
"Speak to Me",
"Breathe",
"On the Run"
]
}SlugRelatedField
SlugRelatedField may be used to represent the target of the relationship using a unique field on the target, typically its slug.
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.SlugRelatedField(
many=True,
slug_field='slug',
queryset=Track.objects.all()
)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']{
"album_name": "The Dark Side of the Moon",
"artist": "Pink Floyd",
"tracks": [
"speak-to-me",
"breathe",
"on-the-run"
]
}This keeps opaque IDs out of your payloads entirely. Clients send "tracks": ["speak-to-me"] on write and DRF resolves the Track objects by their slugs. The target field does not have to be a SlugField - any unique field on the related model works.
NOTE
slug_field is always required. Unless the field is read_only=True, a queryset is also required so incoming values can be resolved. If the value does not match any object, or matches more than one, DRF raises a validation error (400) rather than returning a 404.
HyperlinkedRelatedField
HyperlinkedRelatedField may be used to represent the target of the relationship using a hyperlink.
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name='track-detail'
)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']{
"album_name": "The Dark Side of the Moon",
"artist": "Pink Floyd",
"tracks": [
"http://api.example.com/tracks/89/",
"http://api.example.com/tracks/90/",
"http://api.example.com/tracks/91/"
]
}NOTE
By default, HyperlinkedRelatedField expects a view named '{model_name}-detail'. You can override this using the view_name argument.
Depth
If you only need a quick, read-only nested representation of your relationships, ModelSerializer provides a depth option in the Meta class.
class AlbumSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
depth = 1By setting depth = 1, DRF will automatically traverse all relationships one level deep and serialize them. This is an incredibly convenient shortcut, but it is strictly read-only and you cannot configure exactly which fields are included from the nested model.
Nested Serializers
If you want to fully populate relationships with control over the fields, or you want to support writes, you can use serializers as fields. This allows you to nest serializers to arbitrary depths.
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['id', 'order', 'title', 'duration']
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']{
"album_name": "The Dark Side of the Moon",
"artist": "Pink Floyd",
"tracks": [
{
"id": 89,
"order": 1,
"title": "Speak to Me",
"duration": 67
},
{
"id": 90,
"order": 2,
"title": "Breathe",
"duration": 169
}
]
}Writable Nested Serializers
Django REST Framework does not support writable nested serializers out of the box. While the serializer will accept and validate the nested data, the base ModelSerializer will raise an error if you try to save it because it doesn't automatically know how to handle the child relationships.
To support write operations for a nested serializer, you must ensure the field does not have read_only=True and explicitly override the create() and/or update() methods to handle saving the nested objects yourself.
Furthermore, if you need to support updating existing nested objects, you'll need to make the primary key field available on the nested serializer, without enforcing uniqueness. Adding id = serializers.IntegerField(required=False) achieves this.
from django.db import transaction
class TrackSerializer(serializers.ModelSerializer):
# This allows incoming tracks to be matched to existing database rows on update()
id = serializers.IntegerField(required=False)
class Meta:
model = Track
fields = ['id', 'order', 'title', 'duration']
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
@transaction.atomic
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
# Let the base class handle creating the Album
album = super().create(validated_data)
for track_data in tracks_data:
track_data.pop('id', None)
Track.objects.create(album=album, **track_data)
return album
@transaction.atomic
def update(self, instance, validated_data):
tracks_data = validated_data.pop('tracks', None)
# Let the base class handle updating the Album fields
instance = super().update(instance, validated_data)
# Update the nested tracks
if tracks_data is not None:
existing_tracks = {track.id: track for track in instance.tracks.all()}
seen_track_ids = set()
for track_data in tracks_data:
track_id = track_data.get('id')
if track_id in existing_tracks:
# Update existing track dynamically
track = existing_tracks[track_id]
for attr, value in track_data.items():
if attr != 'id':
setattr(track, attr, value)
track.save()
seen_track_ids.add(track_id)
else:
# Create new track
track_data.pop('id', None)
Track.objects.create(album=instance, **track_data)
# Bulk delete omitted tracks
missing_ids = set(existing_tracks.keys()) - seen_track_ids
if missing_ids:
instance.tracks.filter(id__in=missing_ids).delete()
return instance@transaction.atomic is essential here. Without it, a failure while saving the third track leaves the album persisted with an incomplete set of tracks and returns a 500 to the client. Wrapping both methods means the whole write succeeds or none of it does.
WARNING
This update() deletes any track missing from the payload. That is correct for PUT, but it makes PATCH destructive: a request sending a partial tracks list will remove every track it omits. If your API supports PATCH, either reject partial nested lists or match on id without deleting.
TIP
Most projects do not hand-roll this. The drf-writable-nested package provides WritableNestedModelSerializer, which handles nested creates, updates and deletes for you, reducing the class above to just its Meta block.
