[Django]-Django Rest Framework: How to pass data to a nested Serializer and create an object only after custom validation

3πŸ‘

βœ…

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.

1πŸ‘

When you call serializer.is_valid(raise_exception=True/False) it automatically call validate functions of nested serializer. When you call serializer.save(**kwargs) serializer passes validated data into your create(self, validated_data) or update(self, instance, validated_data) functions of serializer. Moreover, in validated data your ForeignKey fields returned an object.

def create(self, validated_data):
    pages_data = validated_data.pop('pages') # [{'link': Linkobject, ...}]
    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) # here you are sending object to validation again
        if page_serializer.is_valid():
            page_serializer.save()
        else:
            raise serializers.ValidationError(page_serializer.errors)
    return book

Leave a comment