Django Rest Framework- can I allow pk id or full objects in a serializer's create method?

Viewed 1390

Lets say I have the following models:

class Author(models.Model):
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)

class Book(models.Model):
    title = models.CharField(max_length=64)
    author = models.ForeignKeyField(Author, on_delete=models.CASCADE)

And I have the following serializer:

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

If I then query my books, A book's data looks like:

{
    "id": 1,
    "title": "Book Title",
    "author": 4
}

Which is what I want, as I return both an array of books, as well as an array of authors, and allow the client to join everything up. This is because I have many authors that are repeated across books.

However, I want to allow the client to either submit an existing author id to create a new book, or all of the data for a new author. E.g.:

Payload for new book with existing author:

{
    "title": "New Book!",
    "author": 7
}

or, payload for a new book with a new author:

{
    "title": "New Book!",
    "author": {
        "first_name": "New",
        "last_name": "Author"
    }
}

However the second version, will not pass the data validation step in my serializer. Is there a way to override the validation step, to allow either an author id, or a full object? Then in my serializer's create method, I can check the type, and either create a new author, get its id, and create the new book, or just attach the existing id. Thoughts?

2 Answers

I believe that it is not possible to do it in the way you want ( using one field author).

It just because one serializer cannot handle two different types for one field.

Note: i might be wrong about the previous statement.

However, the following is a potential solution for you. You just need to use different field name to create new author.

class BookSerializer(serializers.ModelSerializer):
    author = serializers.PrimaryKeyRelatedField(
        required=False,
        queryset=Author.objects.all(),
    )

    author_add = AuthorSerializer(write_only=True, required=False)

    class Meta:
        model = Book
        fields = ('id', 'title', 'author', 'author_add')
        read_only_fields = ('id')

    def create(self, validated_data):
        author_add_data = validated_data.pop('author_add', None)

        if author_add is not None:
            validated_data['author'] = Author.objects.create(**author_add_data)

        return super().create(validated_data)

Note: you need to handle a case where you send both author and author_add. Probably add a check into validation step and raise ValidationError if both are provided.

Offtopic hint: you dont need to explicityl state read_only_fields = ('id',) - primary keys are read-only.

For anyone else trying to do this, here is what I ended up getting working.

For my book serializer I did the following:

class BookSerializer(serializers.ModelSerializer):
    # make author a foreign key/id, read-only field so that it isn't
    # processed by the validator, and on read returns just the id.
    class Meta:
        model = Book
        fields = ('id', 'title', 'author')
        read_only_fields = ('id', 'author',)

    # override run_validation to validate our author
    def run_validation(self, data):
        # run base validation.  Since `author` is read_only, it will
        # be ignored.
        value = super(Book, self).run_validation(data)
        # inject our validated author into the validated data
        value['author'] = self.validate_author(data['author'])
        return value

    # Custom author validation
    def validate_author(self, author):
        errors = OrderedDict()
        if isinstance(author, int): # if just a key, retrieve the author
            try:
                author_instance = Author.objects.get(pk=author)
            except Author.DoesNotExist:
                errors['author'] = "Author with pk {} does not exist.".format(author)
                raise ValidationError(errors)
        else: # if passed an author object...
            author_serializer = AuthorSerializer(data=author, many=False)
            author_serializer.is_valid(raise_exception=True)
            author_instance = author_serializer.save()

        return author_instance

I need to do a bit more error checking (e.g.- no author passed), but it works quite well- the consumer of the API can submit either an author id, or a serialized author object to create a new author. And the API itself returns just an id as was needed.

Related