Useful packages to have nested serializers in Django-REST

Amir Ayat
6 min readApr 15, 2023

--

All you need to know for a better nested serializer.

Please check the source code on GitHub.

Useful packages to have nested serializers in Django-REST

What is a serializer?

Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data. [link]

In most cases, serializers are completely straightforward and there’s nothing to go wrong, but sometimes we have to represent more complicated objects, where some of the attributes of an object might be used to represent relationships where one object type is nested under the field name of the relation object.

What is a nested serializer?

Although flat data structures serve to properly delineate between the individual entities in your service, there are cases where it may be more appropriate or convenient to use nested data structures. [link]

The Serializer class is itself a type of Field, and can be used to represent relationships where one object type is nested inside another. [link]

By default, doing serialization in this way will give you a read-only nested serializer. Things can get tricky when you are going to have complex nested data objects or support multiple creates and updates.

In these cases, there are two solutions:
use of third-party packages
do it by yourself

In the rest of this article, I will introduce you to some useful packages which do nested object serialization for you. Don’t forget to write a full CRUD support nested serializer by yourself to get familiar with the process.

Write some code

This repository contains a simple project that demonstrates how to use nested serializers. It is a RESTful API to retrieve a list of world countries, states, and cities. We’ll use some third-party packages to represent data in a nested format.

Let’s get familiar with the project:

models.py

from django.db import models
from django.utils.translation import gettext as _


class Country(models.Model):
"""
class model for countries
"""
name = models.CharField(max_length=36)
iso3 = models.CharField(max_length=3)
iso2 = models.CharField(max_length=2)
numeric_code = models.CharField(max_length=3)
phone_code = models.CharField(max_length=16)
capital = models.CharField(max_length=20, null=True)
currency = models.CharField(max_length=3)
currency_name = models.CharField(max_length=39)
currency_symbol = models.CharField(max_length=6)
tld = models.CharField(max_length=3)
native = models.CharField(max_length=50, null=True)
region = models.CharField(max_length=8, null=True)
subregion = models.CharField(max_length=25, null=True)
timezones = models.JSONField(default=list)
translations = models.JSONField()
latitude = models.CharField(max_length=12)
longitude = models.CharField(max_length=13)
emoji = models.CharField(max_length=2)
emojiU = models.CharField(max_length=15)

class Meta:
db_table = 'countries'


class State(models.Model):
"""
class model for states
"""
country = models.ForeignKey(
Country, related_name=_('country_state'), on_delete=models.CASCADE)
name = models.CharField(max_length=56)
country_code = models.CharField(max_length=2)
country_name = models.CharField(max_length=32)
state_code = models.CharField(max_length=5)
type = models.CharField(max_length=45, null=True)
latitude = models.CharField(max_length=12, null=True)
longitude = models.CharField(max_length=13, null=True)

class Meta:
db_table = 'states'


class City(models.Model):
"""
class model for cities
"""
state = models.ForeignKey(
State, related_name=_('state_city'), on_delete=models.CASCADE)
country = models.ForeignKey(
Country, related_name=_('country_city'), on_delete=models.CASCADE)
name = models.CharField(max_length=59)
state_code = models.CharField(max_length=5)
state_name = models.CharField(max_length=56)
country_code = models.CharField(max_length=2)
country_name = models.CharField(max_length=32)
latitude = models.CharField(max_length=12)
longitude = models.CharField(max_length=13)
wikiDataId = models.CharField(max_length=10, null=True)

class Meta:
db_table = 'cities'

1. drf-nested-routers

This package provides routers and relations to create nested resources in the Django Rest Framework.

URL pattern for world countries, states, and cities using drf-nested-routes
URL pattern for world countries, states, and cities using drf-nested-routes

serializers.py

from rest_framework.serializers import ModelSerializer
from world.models import Country, State, City


class CityModelSerializer(ModelSerializer):
"""
serializer for cities
"""

class Meta:
model = City
fields = '__all__'


class StateModelSerializer(ModelSerializer):
"""
serializer for states
"""

class Meta:
model = State
fields = '__all__'


class CountryModelSerializer(ModelSerializer):
"""
serializer for countries
"""

class Meta:
model = Country
fields = '__all__'

views.py

from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework.exceptions import NotFound
from world.models import Country, State, City
from world.serializers import (CountryModelSerializer,
StateModelSerializer,
CityModelSerializer)


class CountryListViewSet(ReadOnlyModelViewSet):
"""
countries view
"""
permission_classes = (AllowAny,)
serializer_class = CountryModelSerializer
queryset = Country.objects.all()


class StateListViewSet(ReadOnlyModelViewSet):
"""
states of a country
"""
permission_classes = (AllowAny,)
serializer_class = StateModelSerializer
queryset = State.objects.all()

def list(self, request, country_pk=None):
queryset = self.filter_queryset(
self.get_queryset().filter(country_id=country_pk))

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

def retrieve(self, request, pk=None, country_pk=None):
try:
instance = self.get_queryset().\
filter(country_id=country_pk).get(pk=pk)
except:
raise NotFound
serializer = self.get_serializer(instance)
return Response(serializer.data)


class CityListViewSet(ReadOnlyModelViewSet):
"""
cities of a state
"""
permission_classes = (AllowAny,)
serializer_class = CityModelSerializer
queryset = City.objects.all()

def list(self, request, country_pk=None, state_pk=None):
queryset = self.filter_queryset(self.get_queryset().\
filter(country_id=country_pk, state_id=state_pk))

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

def retrieve(self, request, pk=None, country_pk=None, state_pk=None):
try:
instance = self.get_queryset().filter(
country_id=country_pk, state_id=state_pk).get(pk=pk)
except:
raise NotFound
serializer = self.get_serializer(instance)
return Response(serializer.data)

urls.py

from rest_framework_nested import routers
from world.views import (CountryListViewSet,
StateListViewSet,
CityListViewSet)


# nested routers
country_router = routers.SimpleRouter()
country_router.register(r'country', CountryListViewSet)

state_nested_router = routers.NestedSimpleRouter(country_router,
r'country',
lookup='country')
state_nested_router.register(r'state', StateListViewSet)

city_nested_router = routers.NestedSimpleRouter(state_nested_router,
r'state',
lookup='state')
city_nested_router.register(r'city', CityListViewSet)

urlpatterns = [
path('nested/', include(country_router.urls)),
path('nested/', include(state_nested_router.urls)),
path('nested/', include(city_nested_router.urls))
]

2. drf-flex-fields

FlexFields (DRF-FF) for Django REST Framework is a package designed to provide a common baseline of functionality for dynamically setting fields and nested models within DRF serializers. This package is designed for simplicity, with minimal magic and entanglement with DRF’s foundational classes.

Dynamic relation fields with def-flex-fields
Dynamic relation fields with drf-flex-fields

serializers.py

from rest_framework.serializers import ModelSerializer
from rest_flex_fields import FlexFieldsModelSerializer
from world.models import Country, State, City


class StateModelSerializer(ModelSerializer):
"""
serializer for states
"""

class Meta:
model = State
fields = '__all__'


class CountryModelSerializer(ModelSerializer):
"""
serializer for countries
"""

class Meta:
model = Country
fields = '__all__'


class StateFlexSerializer(FlexFieldsModelSerializer):
"""
flex serializer for states
"""

class Meta:
model = State
fields = '__all__'
expandable_fields = {
'country': CountryModelSerializer
}


class CityFlexSerializer(FlexFieldsModelSerializer):
"""
flex serializer for cities
"""

class Meta:
model = City
fields = '__all__'
expandable_fields = {
'state': StateModelSerializer,
'country': CountryModelSerializer
}

views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework.pagination import LimitOffsetPagination
from rest_flex_fields import is_expanded
from world.models import State, City
from world.serializers import CityFlexSerializer, StateFlexSerializer


class StateFlexViewSet(ModelViewSet):
"""
list of states and their country
example query parameters
?limit=10&offset=10&expand=country&fields=id,name,country
"""
pagination_class = LimitOffsetPagination
permission_classes = (AllowAny,)
serializer_class = StateFlexSerializer
queryset = State.objects.all()

def get_queryset(self):
queryset = self.queryset
if is_expanded(self.request, 'country'):
queryset = queryset.select_related('country')
return queryset


class CityFlexViewSet(ModelViewSet):
"""
list of cities and their country and state
example query parameters
?limit=10&offset=10&expand=country,state&fields=id,name,country,state
"""
pagination_class = LimitOffsetPagination
permission_classes = (AllowAny,)
serializer_class = CityFlexSerializer
queryset = City.objects.all()

def get_queryset(self):
queryset = self.queryset
if is_expanded(self.request, 'country'):
queryset = queryset.select_related('country')
if is_expanded(self.request, 'state'):
queryset = queryset.select_related('state')
return queryset

urls.py

from rest_framework import routers
from world.views import CityFlexViewSet, StateFlexViewSet


# expandable routers
state_flex_router = routers.SimpleRouter()
state_flex_router.register(r'state', StateFlexViewSet)

city_flex_router = routers.SimpleRouter()
city_flex_router.register(r'city', CityFlexViewSet)

urlpatterns = [
path('expandable/', include(state_flex_router.urls)),
path('expandable/', include(city_flex_router.urls))
]

3. drf-writable-nested

This is a writable nested model serializer for Django REST Framework which allows you to create/update your models with related nested data.

Writable relation fields with drf-writable-nested
Writable relation fields with drf-writable-nested

serializers.py

from rest_framework.serializers import ModelSerializer
from drf_writable_nested import WritableNestedModelSerializer
from world.models import Country, State, City


class CityModelSerializer(ModelSerializer):
"""
serializer for cities
"""

class Meta:
model = City
fields = '__all__'


class StateModelSerializer(ModelSerializer):
"""
serializer for states
"""

class Meta:
model = State
fields = '__all__'


class StateNestedModelSerializer(WritableNestedModelSerializer):
"""
nested serializer for states and their cities
"""
state_city = CityModelSerializer(many=True)

class Meta:
model = State
fields = [
'name',
'country_code',
'country_name',
'state_code',
'type',
'latitude',
'longitude',
'state_city'
]


class CountryNestedModelSerializer(WritableNestedModelSerializer):
"""
nested serializer for countries and their states
"""
country_state = StateNestedModelSerializer(many=True)

class Meta:
model = Country
fields = [
'name',
'iso3',
'iso2',
'numeric_code',
'phone_code',
'capital',
'currency',
'currency_name',
'currency_symbol',
'tld',
'native',
'region',
'subregion',
'timezones',
'translations',
'latitude',
'longitude',
'emoji',
'emojiU',
'country_state'
]

views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework.pagination import LimitOffsetPagination
from world.models import Country
from world.serializers import CountryNestedModelSerializer


class CountryWritableViewSet(ModelViewSet):
"""
list of counties and their states and cities
"""
pagination_class = LimitOffsetPagination
permission_classes = (AllowAny,)
serializer_class = CountryNestedModelSerializer
queryset = Country.objects.\
prefetch_related('country_state__state_city')

urls.py

from rest_framework_nested import routers
from world.views import CountryWritableViewSet


# writable nested router
country_nested_router = routers.SimpleRouter()
country_nested_router.register(r'country', CountryWritableViewSet)

urlpatterns = [
path('writable/', include(country_nested_router.urls))
]

--

--