Search code examples
djangodjango-modelsdjango-rest-frameworkdjango-viewsdjango-serializer

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


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?


Solution

  • 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.