Representing relationships with nested serializers

A nurse carrying a tray with a tin of "Django"-branded chocolate on top. On the tin is a picture of the same nurse, carrying the same tray.

When working with Django it’s quite common to use the ORM to represent relationships between model classes. For example, you might model a relationship between an Artist and an Album using a ForeignKey field. However, how should we represent these relationships if we’re using Django Rest Framework to build a REST API?

One way to represent these kinds of relationships is by using nested serializers, which is the technique I’ll show you in this tutorial.

Prerequisites

If you want to follow along with this tutorial, install Django REST Framework and make sure you’ve added "rest_framework" to your INSTALLED_APPS list.

The setup

Let’s say we’re creating a music database that contains a list of artists and their albums. For this tutorial I’ve created a musicdb Django application, with a models.py file that looks like this:

# musicdb/models.py

from django.db import models


class Artist(models.Model):
    name = models.CharField(max_length=32)

    def __str__(self):
        return self.name


class Album(models.Model):
    artist = models.ForeignKey(
        Artist,
        related_name="albums",
        on_delete=models.CASCADE,
    )
    title = models.CharField(max_length=32)
    genre = models.CharField(max_length=16)
    release_date = models.DateField()
    length = models.DurationField()

    def __str__(self):
        return f"{self.title} ({self.year})"

In summary, each Artist can have several Albums, where each album has a title, a genre, a release date and a length. Note that I’ve given the “artist” ForeignKey the related name "albums"; this means if we have an Artist model instance we can get a queryset of their albums via artist.albums.

Creating serializers

In order to access these models through Django Rest Framework we’ll create a serializer for each model:

# musicdb/serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Album, Artist


class AlbumSerializer(ModelSerializer):

    class Meta:
        model = Album
        fields = [
            "id",
            "artist",
            "title",
            "genre",
            "release_date",
            "length",
        ]


class ArtistSerializer(ModelSerializer):

    class Meta:
        model = Artist
        fields = ["id", "albums", "name"]

As we’re using ModelSerializer we only need to point out which model we want to serialize, together with the model fields we want to include.

Creating views

In order to make our API accessible from the outside we need to create a few views. Since we just want to expose our serializers we can use the built-in ModelViewSet class:

# musicdb/views.py

from rest_framework.viewsets import ModelViewSet

from .models import Album, Artist
from .serializers import (
    AlbumSerializer,
    ArtistSerializer,
)

class ArtistViewSet(ModelViewSet):
    serializer_class = ArtistSerializer
    queryset = Artist.objects.all()


class AlbumViewSet(ModelViewSet):
    serializer_class = AlbumSerializer
    queryset = Album.objects.all()

This will automatically create views for creating, fetching, updating and deleting Artists and Albums.

Before we can access our views we need to hook them up to some URLs. We’ll open up our project’s urls.py file and add the following code:

# project/urls.py

from django.contrib import admin
from django.urls import path
from rest_framework.routers import DefaultRouter

from musicdb.views import AlbumViewSet, ArtistViewSet

router = DefaultRouter()
router.register("albums", AlbumViewSet)
router.register("artists", ArtistViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
] + router.urls

(The “admin”-related views are generated by Django; you can leave them out if you want)

Trying it out

If you start the development server with manage.py runserver and go to http://localhost:8000/ you’ll end up in DRF’s Browsable API. The “root” endpoint should list all the available API endpoints, i.e. albums and artists. To get some data to experiment with, I used the Browsable API to create an artist and a few albums.

The output of e.g. http://localhost:8000/artists/1 should now look something like this:

{
    "id": 1,
    "albums": [ 1, 2, 3 ],
    "name": "The Wild Gooseberries"
}

The "albums": [ 1, 2, 3 ] part means the albums with IDs 1, 2 and 3 belong to this particular artist.

Let’s have a look at the album with ID: 1 (it should be at http://localhost:8000/albums/1/):

{
    "id": 1,
    "artist": 1,
    "title": "Lost in Space Jazz",
    "genre": "Jazz",
    "release_date": "2021-10-11",
    "length": "00:45:10"
}

As you can see, the “album-to-artist” relation is also represented as an ID.

While representing relationships as IDs can work, it’s usually not optimal:

  • It makes the API hard to understand for developers; if we’re casually browsing the API, something like "artist": 1 doesn’t really tell us anything useful about the artist, or how to fetch more information about them.
  • If we want to present a list of an artist’s albums in a user interface we’ll likely want to display some information about each album (e.g. the title, release date, etc.). If all we have is a list of IDs we have to make additional requests to the server to get that information.

Let’s see if we can’t “jazz it up” a little bit!

Nesting serializers

Instead of justing listing the IDs of an artist’s albums wouldn’t it be neat if we could output the title and release date instead? We can accomplish this by nesting serializers, i.e. putting another serializer inside the ArtistSerializer.

Now, we could re-use our existing AlbumSerializer for this purpose, but I usually prefer creating a specific serializer with fewer fields when using nested serializers.

Note

If you don’t care about customizing the nested serializer you can also just use the depth option—this tells Django Rest Framework to keep serializing nested objects up until the given depth.

Open up serializers.py and rename the existing AlbumSerializer to AlbumDetailSerializer. Then add a new serializer class just below the import statements:

# musicdb/serializers.py

class AlbumSerializer(HyperlinkedModelSerializer):
    class Meta:
        model = Album
        fields = ["title", "release_date", "url"]

Instead of inherting from ModelSerializer the new serializer uses HyperlinkedModelSerializer as its base class, so we’ll need to add it to our list of imports (it’s in the same rest_framework.serializers namespace as the other serializers).

HyperlinkedModelSerializer will add an automatically generated url attribute to our serializer, which is a link pointing to the object’s “detail” page. This is good because it lets both human users and API clients know where they can go to fetch additional information about an object.

Finally, let’s add our new serializer as a field on ArtistSerializer:

# musicdb/serializers.py

class ArtistSerializer(ModelSerializer):
    albums = AlbumSerializer(many=True, read_only=True)

    class Meta:
        model = Artist
        fields = ["id", "albums", "name"]

Note that nested serializers are read-only by default; however, the Browsable API will still render an input form for the nested serializer in case you would want to customize it to make it writable. We won’t bother with that in this tutorial, so I’ve disabled the input form for albums by explicitly setting the read_only option to True.

Tip

If you want to make your nested serializers writable, see the section on Writable nested representations in the Django REST Framework docs.

Action-specific serializers

Before we can try out our nested serializer we need to make a few changes to ArtistViewSet; since we changed the name of our original “detail” serializer the views will currently use the “summary” serializer instead.

We could fix this by just changing the serializer_class attribute, but wouldn’t it be neat if we could use the “summary” serializer for our list views as well? We can accomplish this by overriding the get_serializer_class method on our viewset like this:

class ArtistViewSet(ModelViewSet):
    queryset = Artist.objects.all()

    def get_serializer_class(self):
        if self.action == "list":
            return ArtistSerializer
        return ArtistDetailSerializer

(Don’t forget to add ArtistDetailSerializer to the list of imports as well!)

This means that if we fetch the artist “list view” (i.e. http://localhost:8000/artists) the view will use ArtistSerializer, and for all other views it will use ArtistDetailSerializer.

If we go back and fetch our artist again the output should now look something like this:

{
    "id": 1,
    "albums": [
        {
            "title": "Lost In Space Jazz",
            "release_date": "2001-10-11",
            "url": "http://localhost:8000/albums/1/"
        },
        {
            "title": "Cloudberry Jam",
            "release_date": "2008-04-04",
            "url": "http://localhost:8000/albums/2/"
        },
        {
            "title": "In Stereo!",
            "release_date": "2011-01-22",
            "url": "http://localhost:8000/albums/3/"
        }
    ],
    "name": "The Wild Gooseberries"
}

Much better! Now we get to see some information about the albums, and if we need more details about an album each list item has a link to the album’s “detail” page.

Let’s have a look at the artist “list view” to see what it looks like:

[
    {
        "name": "The Wild Gooseberries",
        "url": "http://localhost:8000/artists/1/"
    }
]

Neat! This means we won’t clutter up our list views with details about each artist; it’s also more performant than using the ArtistDetailSerializer as we don’t need to make any extra database calls to fetch the albums for each artist.

Linking artists to albums

Now that we’ve provided a bit more context for the albums in the artist serializer, could we do something similar with the album serializer? As you might remember, if we fetched a single album it looked something like this:

{
    "id": 1,
    "artist": 1,
    "title": "Lost in Space Jazz",
    "genre": "Jazz",
    "release_date": "2021-10-11",
    "length": "00:45:10"
}

At the very least it would be nice to know who "artist": 1 is supposed to be; having a URL to the artist’s detail page also wouldn’t hurt.

Just like we did with the ArtistSerializer, let’s rename AlbumSerializer to AlbumDetailSerializer and add a new serializer class above the other serializers:

# musicdb/serializers.py

class ArtistSerializer(HyperlinkedModelSerializer):
    class Meta:
        model = Artist
        fields = ["name", "url"]

When creating or editing albums it’s quite convenient to have the artist field on the serializer refer to the artist’s ID, so we’ll leave that field alone. Instead, let’s add an artist_info field to AlbumDetailSerializer like this:

# musicdb/serializers.py

class AlbumDetailSerializer(ModelSerializer):
    artist_info = ArtistSerializer(source="artist", read_only=True)

    class Meta:
        model = Album
        fields = [
            "id",
            "artist",
            "artist_info",
            "title",
            "genre",
            "release_date",
            "length",
        ]

Since there’s no corresponding artist_info field on the Album model we have to tell ArtistSerializer where to get its data from using the source attribute.

If we fetch the album again it should now look something like this:

{
    "id": 1,
    "artist": 1,
    "artist_info": {
        "name": "The Wild Gooseberries",
        "url": "http://localhost:8000/artists/1/"
    },
    "title": "Cloudberry Jam",
    "genre": "Jazz",
    "release_date": "2001-10-11",
    "length": "00:45:10"
}

Wrapping up

We’ve now come full circle as we can navigate from an artist to the artist’s albums, and get back to the artist again by following a link from one of their albums.

Using nested serializers like this works well when we’re dealing with limited numbers of items; unless you count outliers like Frank Zappa it’s rare that artists release more than a few dozen albums during their careers. However, what if we wanted to represent larger number of items, such as a user’s Twitter followers? Some accounts have followers numbered in millions, and it wouldn’t be practical to enumerate all of them in a single response.

In these cases it’s usually better to represent the relationship with a nested set of URLs. For example, instead of putting the entire list of followers in a single response, we can make them available at a /:username/followers route. This means the related entities can be fetched separately, and we can add pagination if the list of entities becomes too large. If this sounds interesting you should check out my post on nested routes—otherwise I’ll catch you later!

Want more posts like this, delivered straight to your inbox? Subscribe to my newsletter!