I have two models as:
class Book(AppModel):
title = models.CharField(max_length=255)
class Link(AppModel):
link = models.CharField(max_length=255)
class Page(AppModel):
book= models.ForeignKey("Book", related_name="pages", on_delete=models.CASCADE)
link = models.ForeignKey("Link", related_name="pages", on_delete=models.CASCADE)
page_no = models.IntegerField()
text = models.TextField()
and serializers
class LinkSerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ['link']
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
@transaction.atomic
def create(self, validated_data):
pages_data= validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
Page.objects.create(book=book, **page_data)
return book
There is a validate_text
method in PageSerializer
. The create
method will never call the PageSerializer
and the page_data
is never validated.
So I tried another approach as:
@transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid():
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors)
return book
Posted data:
{
"title": "Book Title",
"pages": [
{
"link": 1, "page_no": 52, "text": "sometext"
}
]
}
But the above approach throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
I also found why this error is caused: Though I am posting data with pk
value 1
of a Link
, the data when passed to the PageSerializer
from the BookSerializer
appears as such: {"link": "/go_to_link/", "page_no":52, "text": "sometext"}
Why is an instance of Link
passed to the PageSerializer
whereas what I sent is pk
of Link
?
To validate a nested object using a nested serializer:
@transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages') #pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
Suppose you send the data as:
{
"title": "Book Title",
"pages": [{
"link":2,#<= this one here
"page_no":52,
"text":"sometext"}]
}
In the above data we are sending an id
of the Link
object. But in the create
method of the BookSerializer
defined above, the data we sent changes to:
{
"title": "Book Title",
"pages": [{
"link":Link Object (2),#<= changed to the Link object with id 2
"page_no":52,
"text":"sometext"}]
}
And the PageSerializer
is actually meant to receive an pk
value of the link
i.e, "link": 2
instead of "link":Link Object (2)
. Hence it throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
So the workaround is to override the to_internal_value
method of the nested serializer to convert the received Link Object (2)
object to its pk
value.
So your PageSerializer
class should then be:
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def to_internal_value(self, data):
link_data = data.get("link")
if isinstance(link_data, Link): #if object is received
data["link"] = link_data.pk # change to its pk value
obj = super(PageSerializer, self).to_internal_value(data)
return obj
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
and the parent serializer:
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
@transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')#pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
That should allow the nested serializer to do the validation instead of writing validation inside the create method of the parent serializer and violating DRY.