Neil
Neil

Reputation: 3291

Django Rest Framework creating child objects in parent serializer using child serializer

Supposing some standard Django relational setup like this:

models.py

class Book(models.Model):
    title = models.CharField(max_length=30)

class Page(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    text = models.CharField(max_length=100)

I'd like to create a book and all its pages with one request. If we start with serializers like this:

serializers.py

class PageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Page
        fields = '__all__'

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ('title', 'pages')

    pages = PageSerializer(many=True)

Then the problem is that the PageSerializer now requires a book foreign key. But I don't know the key of the book until I've created the book, which is only after I've sent the POST request. So I cannot include the book pk in the POST data that the client sends.

An obvious solution is to override the create function on the Book serializer. But then I am still faced with the problem that the validators will say that the book field is required and the POST data will fail to validate.

I could make book a not-required field on the PageSerialzer. But this seems very bad. The book field IS required. And the BookSerializer create method will be able to supply it. It's just the client that doesn't know it.

So my suspicion is that the best way to do this is to leave book as required on the PageSerializer, but somehow make it so that the validators on the BookSerializer don't check for whether that is in the POST data when I post to BookSerializer.

Is this the correct way to achieve what I want? And if so, how do I do it? Thank you.

Upvotes: 0

Views: 3335

Answers (2)

Seb
Seb

Reputation: 363

I'd link your ViewSet to a BookCreateSerializer, and from this specific serializer I'd then add a function to not only verify the received data but make sure you link the parent's id to the child's one during creation.

IMPORTANT NOTE

This works if a parent only has one child, not sure about when passing multiple children.

Here is what is could look like.

BookCreateSerializer:

class BookCreateSerializer(serializers.ModelSerializer):
    """
    Serializer to create a new Book model in DB
    """

    pages = PageCreateSerializer()

    class Meta:
        model = Book
        fields = [
            'title',
            'pages'
        ]

    def create(self, validated_data):
        page_data = validated_data.pop('page')
        book = Book.objects.create(**validated_data)
        Page.objects.create(book=book, **page_data)
        return book

PageCreateSerializer

class PageCreateSerializer(serializers.ModelSerializer):
    """
    Serializer to create a new Page model in DB
    """

    class Meta:
        model = Page
        fields = [
            'book',
            'text'
        ]

To make sure that your Book instance understands what a page field is in the serializer, you have to define a related_name in its child's Model (Page). The name you choose is up to you. It could look like:

class Page(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='page')
    text = models.CharField(max_length=100)

Upvotes: 2

reedonne
reedonne

Reputation: 96

Why not try handling it in the create viewset. You can validate the data for the Book object first, before creating it. Then validate the data for the Page object using the created Book object and the other data sent from the request to the page.

Upvotes: 3

Related Questions