Dynamic Serialization With Django Rest Framework
Suppose we have two django models, Product
and Size
, defined as follows:
class Product(models.Model):
name = models.CharField(max_length=99)
description = models.TextField(null=True, blank=True)
class Size(models.Model):
product = models.ForeignKey('Product', related_name='sizes', on_delete=models.CASCADE)
code = models.CharField(max_length=10)
text = models.CharField(max_length=20)
quantity = models.PositiveIntegerField(default=1)
On a product detail view page, we might want to return a JSON response similar to this:
{
"id": 1,
"name": "Fur Flight Jacket",
"description": "Jacket made of textured tear-resistant ripstop fabric",
"sizes": [
{ "code": "xs", "text": "Extra-Small", "quantity": 15 },
{ "code": "s", "text": "Small", "quantity": 20 },
{ "code": "m", "text": "Medium", "quantity": 0 },
{ "code": "l", "text": "Large", "quantity": 10 },
{ "code": "xl", "text": "Extra-Large", "quantity": 5 }
]
}
This response includes not only the id
, name
, and description
fields of a product, but also a list of its available sizes.
However, on a product list page, we might prefer a simpler response that only includes the id, name, and description fields, like this:
[
{
"id": 1,
"name": "Flight Jacket",
"description": "Jacket made of textured tear-resistant ripstop fabric"
},
{
"id": 2,
"name": "Stretch Linen Dress",
"description": "A dress made of stretch linen blend fabric. Halter neck with bead appliqué"
},
{
"id": 3,
"name": "Denim Jacket",
"description": "Lapel collar jacket with long sleeves with buttoned cuffs"
}
]
The easiest, and probably recommended way to handle these two different responses, is to create two separate serializers, ProductListSerializer
and ProductDetailSerializer
.
For example, ProductListSerializer would be defined like this:
class ProductListSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields=('id','name', 'description')
ProductDetailSerializer would be defined like this:
class ProductDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields=('id','name', 'description','sizes')
sizes = SizeSerializer(many=True, read_only=True)
Note that ProductDetailSerializer includes the sizes field, which is a list of Size objects related to the Product. Finally, SizeSerializer would look like this:
class SizeSerializer(serializers.ModelSerializer):
class Meta:
model = Size
fields = ('code','text', 'quantity')
Alternatively, we can create these serializers dynamically by taking advantage of the type
function which allows you to create a new class dynamically by specifying its name, base classes, and attributes. For example,
def get_product_serializer_class(*, fields:tuple=()):
fields = fields + ('id', 'name')
attrs = {'Meta': type('Meta', (), {'model': Product,'fields': fields,})}
if "sizes" in fields:
attrs['sizes'] = SizeSerializer(many=True, read_only=True)
return type('ProductSerializer', (serializers.ModelSerializer,), attrs)
This function takes a tuple of fields as an argument and returns a new serializer class with those fields included. For example, we could redefine our previous serializers as such:
ProductListSerializer = get_product_serializer_class(fields=('description',))
ProductDetailSerializer = get_product_serializer_class(fields=('description', 'sizes'))
To use either of these serializers, we simply instantiate the class and pass it a Product instance. For example:
product = Product.objects.get(id=1)
print(ProductDetailSerializer(product).data)
We can also use it in a view or viewset as such:
class ProductViewset(viewsets.ModelViewSet):
queryset = Product.objects.active()
serializer_class = ProductSerializer
def get_serializer_class(self):
if self.action == 'list':
return ProductListSerializer
return ProductSerializer
Or maybe, you want to select the fields based on a request’s query parameters
class ProductViewset(viewsets.ModelViewSet):
queryset = Product.objects.active()
serializer_class = ProductSerializer
def get_serializer_class(self):
# getattr(request, 'query_params', {})
fields = set()
for item in self.request.query_params.getlist('fields'):
fields.update(item.split(','))
return get_product_serializer_class(fields=fields)
Keep in mind that using this approach has some disadvantages, such as performance overhead and increased complexity, so it should be used with caution. However, it allows us to create a product serializer for any set of fields we need without having to define a separate class for each combination of fields.