Please check the source code on GitHub.
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 ofField
, 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.
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.
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.
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))
]