20

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?

him229
  • 1,284
  • 1
  • 11
  • 21
  • Look at http://www.django-rest-framework.org/api-guide/routers/ - add decorators in correspondence with your needs. – dmitryro Jul 11 '16 at 21:17
  • @dmitryro I don't understand. Could you please explain further? How would adding decorators modify fields for serializers? – him229 Jul 11 '16 at 21:24
  • You have to create a custom router that will handle different request methods - POST, GET, PUT , and decorate your methods based on what request method you want to use - documentation provides some samples. Also see this http://stackoverflow.com/questions/28957912/overriding-django-rest-viewset-with-custom-post-method-and-model – dmitryro Jul 11 '16 at 21:28

5 Answers5

13

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.

jmoz
  • 7,846
  • 5
  • 31
  • 33
9

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

Community
  • 1
  • 1
Jadav Bheda
  • 5,031
  • 1
  • 30
  • 28
5

You are looking for the get_serializer_class method on the ViewSet. This allows you to switch on request type for which serializer that you want to use.

from rest_framework import viewsets

class MyModelViewSet(viewsets.ModelViewSet):

    model = MyModel
    queryset = MyModel.objects.all()

    def get_serializer_class(self):
        if self.action in ('create', 'update', 'partial_update'):
            return MySerializerWithPrimaryKeysForCreatingOrUpdating
        else:
            return MySerializerWithNestedData
Aaron Lelevier
  • 19,850
  • 11
  • 76
  • 111
  • This is one possible way that could work, but I do not wish to declare two entirely separate serializers as that would be redundant. I was wondering if there was a better way of doing this in one serializer. – him229 Jul 12 '16 at 18:15
2

I know it's a little late, but just in case someone else needs it. There are some third party packages for drf that allow dynamic setting of included serializer fields via the request query parameters (listed in the official docs: https://www.django-rest-framework.org/api-guide/serializers/#third-party-packages).

IMO the most complete ones are:

  1. https://github.com/AltSchool/dynamic-rest
  2. https://github.com/rsinger86/drf-flex-fields

where (1) has more features than (2) (maybe too many, depending on what you want to do).

With (2) you can do things such as (extracted from the repo's readme):

class CountrySerializer(FlexFieldsModelSerializer):
    class Meta:
        model = Country
        fields = ['name', 'population']


class PersonSerializer(FlexFieldsModelSerializer):
    country = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Person
        fields = ['id', 'name', 'country', 'occupation']

    expandable_fields = {
        'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
    }

The default response:

{
  "id" : 13322,
  "name" : "John Doe",
  "country" : 12,
  "occupation" : "Programmer"
}

When you do a GET /person/13322?expand=country, the response will change to:

{
  "id" : 13322,
  "name" : "John Doe",
  "country" : {
    "name" : "United States"
  },
  "occupation" : "Programmer",
}

Notice how population was ommitted from the nested country object. This is because fields was set to ['name'] when passed to the embedded CountrySerializer.

This way you can keep your POST requests including just an id, and "expand" GET responses to include more details.

aleclara95
  • 168
  • 6
0

The way I ended up dealing with this problem was having another serializer for when it's a related field.

class HumanSerializer(PersonSerializer):

    class Meta:
        model = Human
        fields = PersonSerializer.Meta.fields + (
            'firstname',
            'middlename',
            'lastname',
            'sex',
            'date_of_birth',
            'balance'
        )
        read_only_fields = ('name',)


class HumanRelatedSerializer(HumanSerializer):
    def to_internal_value(self, data):
        return self.Meta.model.objects.get(id=data['id'])


class PhoneNumberSerializer(serializers.ModelSerializer):
    contact = HumanRelatedSerializer()

    class Meta:
        model = PhoneNumber
        fields = (
            'id',
            'contact',
            'phone',
            'extension'
        )

You could do something like this, but for the RelatedSerializer do:

 def to_internal_value(self, data):
     return self.Meta.model.objects.get(id=data)

Thus, when serializing, you serialize the related object, and when de-serializing, you only need the id to get the related object.

miyamoto
  • 1,540
  • 10
  • 16
  • Did this. Did not work. Am I doing anything wrong? `class AuthorRelatedSerializer(AuthorSerializer): def to_internal_value(self, data): return self.Meta.model.objects.get(id=data['id']) class BookSerializer(serializers.ModelSerializer): author = AuthorRelatedSerializer()` – him229 Jul 11 '16 at 22:32
  • My example expects {id: 6, ...}, if you're just passing it an integer, it should be `def to_internal_value(self, data): return self.Meta.model.objects.get(id=data)` – miyamoto Jul 11 '16 at 22:38