Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change a field in a Django REST Framework ModelSerializer based on the request type?

Consider this case where I have a Book and Author model.

serializers.py

class AuthorSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.Author
        fields = ('id', 'name')

class BookSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

viewsets.py

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

This works great if I send a GET request for a book. I get an output with a nested serializer containing the book details and the nested author details, which is what I want.

However, when I want to create/update a book, I have to send a POST/PUT/PATCH with the nested details of the author instead of just their id. I want to be able to create/update a book object by specifying a author id and not the entire author object.

So, something where my serializer looks like this for a GET request

class BookSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

and my serializer looks like this for a POST, PUT, PATCH request

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

    class Meta:
        model = models.Book
        fields = ('id', 'title', 'author')

I also do not want to create two entirely separate serializers for each type of request. I'd like to just modify the author field in the BookSerializer.

Lastly, is there a better way of doing this entire thing?

like image 211
him229 Avatar asked Jul 11 '16 21:07

him229


2 Answers

There is a feature of DRF where you can dynamically change the fields on the serializer http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

My use case: use slug field on GET so we can see nice rep of a relation, but on POST/PUT switch back to the classic primary key update. Adjust your serializer to something like this:

class FooSerializer(serializers.ModelSerializer):
    bar = serializers.SlugRelatedField(slug_field='baz', queryset=models.Bar.objects.all())

    class Meta:
        model = models.Foo
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(FooSerializer, self).__init__(*args, **kwargs)

        try:
            if self.context['request'].method in ['POST', 'PUT']:
                self.fields['bar'] = serializers.PrimaryKeyRelatedField(queryset=models.Bar.objects.all())
        except KeyError:
            pass

The KeyError is sometimes thrown on code initialisation without a request, possibly unit tests.

Enjoy and use responsibly.

like image 193
jmoz Avatar answered Oct 10 '22 00:10

jmoz


IMHO, multiple serializers are only going to create more and more confusion.

Rather I would prefer below solution:

  1. Don't change your viewset (leave it default)
  2. Add .validate() method in your serializer; along with other required .create or .update() etc. Here, real logic will go in validate() method. Where based on request type we will be creating validated_data dict as required by our serializer.

I think this is the cleanest approach.

See my similar problem and solution at DRF: Allow all fields in GET request but restrict POST to just one field

like image 25
Jadav Bheda Avatar answered Oct 10 '22 00:10

Jadav Bheda