I ran into an issue using NestedFields when it wraps a serializer that allows no data body when POST
ing.
What I want to be able to achieve is embed the a serializer as a nested serializer in another serializer. With this nested serializer, I want to be able to either:
- Automatically create the instance for the nested serializer if no data is available for the serializer.
- Create the instance if data is available for the nested serializer.
The problem I face is that in order to achieve bullet point 1, I can't wrap the nested serializer field in NestedField
, and I would have to then override create
and update
in the parent serializer to achieve bullet point 2.
As an example, consider the following model definitions and their serializer:
# models.py
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.timezone import now
class LotNumber(models.Model):
lot_number = models.CharField(max_length=6, primary_key=True, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@staticmethod
def _generate_lot_number(length=6):
return get_random_string(length=length)
def save(self, *args, **kwargs):
if not self.lot_number:
self.lot_number = self._generate_lot_number(length=6)
super().save(*args, **kwargs)
class Inventory(models.Model):
sku_code = models.CharField(max_length=32)
lot_number = models.ForeignKey(LotNumber, blank=True, null=True, on_delete=models.PROTECT)
finish_date = models.DateField(default=now)
def save(self, *args, **kwargs):
if not self.lot_number:
lot_class = self._meta.get_field("lot_number").related_model
new_lot = lot_class.objects.create()
self.lot_number = new_lot
super().save(*args, **kwargs)
# serializers.py
from rest_framework import serializers
from django_restql.serializers import NestedModelSerializer
from django_restql.fields import NestedField
from .models import LotNumber, Inventory
class LotNumberSerializer(serializers.ModelSerializer):
class Meta:
model = LotNumber
fields = "__all__"
class InventorySerializer(serializers.ModelSerializer):
lot_number = LotNumberSerializer(required=False)
class Meta:
model = Inventory
fields = "__all__"
class NestedInventorySerializer(NestedModelSerializer):
lot_number = NestedField(LotNumberSerializer, required=False)
class Meta:
model = Inventory
fields = "__all__"
If I were to make a POST
request at the InventorySerializer
endpoint, I would be able to create an Inventory
instance without passing data for the lot_number
field because the save
method in the LotNumber
model automatically creates the data. The LotNumberSerializer
would also consider the empty data as valid. The response would be something like:
{
"id": 1,
"sku_code": "ABC-PRODUCT",
"lot_number": {
"lot_number": "P519CK",
"is_active": true,
"created_at": "2020-03-18T12:12:39.943000Z",
"updated_at": "2020-03-18T12:12:39.943000Z"
}
}
This only achieves bullet point 1 from earlier. If I wanted to inject my own lot_number
value, I have to override the create
and update
methods in InventorySerializer
, which complicates things.
Now, if I were to make a POST
request at the NestedInventorySerializer
endpoint, I would be able to create the Inventory
instance if I did pass data for the lot_number
field. However, if I attempt bullet point 1 on this serializer, I get the following response:
{
"lot_number": {
"non_field_errors": [
"No data provided"
]
}
}
I found a temporary fix whereby requiring the lot_number
field in the parent serializer and setting a default={}
on the field resolved the problem. However I don't think this aligns with the intent of the NestedField
wrapper, where arguments should behave the same way as when the wrapper is not used.
Instead, I think the fix for this is in fields.py
in the to_internal_value
method of the BaseNestedFieldSerializer
class. Specifically in line 278, the code should be data = {}
. Technically, the code could be moved to line 275 and lines 276-278 can be deleted. This fix should not affect scenarios where a nested serializer does require data and data isn't passed, due to the fact that calling is_valid
in this scenario will return False
.