2

I'm implementing some REST API in DRF with ModelViewSet and ModelSerializer. All my APIs use the JSON format and some of my models use ChoiceField field, like that:

MyModel(models.Model):
     KEY1 = 'Key1'
     KEY2 = 'Key2'
     ATTRIBUTE_CHOICES = (
         (KEY1, 'Label 1'),
         (KEY2, 'Label 2'))
     attribute = models.CharField(max_length=4, 
                                  choices=ATTRIBUTE_CHOICES, default=KEY1)

My problem is that by default DRF always returns (and accept) the key of these choices for JSON messages (see here), but I would like to use the label instead, because I think it's more consistent and clear to unterstand for who will use those APIs. Any suggestion?

SimoV8
  • 1,382
  • 1
  • 18
  • 32

3 Answers3

5

I found a possible solution, namely defining my own field as follow:

class MyChoiceField(serializers.ChoiceField):

    def to_representation(self, data):
        if data not in self.choices.keys():
            self.fail('invalid_choice', input=data)
        else:
            return self.choices[data]

    def to_internal_value(self, data):
        for key, value in self.choices.items():
            if value == data:
                 return key
        self.fail('invalid_choice', input=data)

It works the same way as to ChoiceField, but returns and accepts labels instead of keys.

SimoV8
  • 1,382
  • 1
  • 18
  • 32
  • Any reason why you're saving label instead of key? – mariodev Dec 31 '15 at 08:10
  • @mariodev I'm not saving the label, in the db will be stored the key. I'm using the label only for communication with the client, `MyChoiceField` just map values with keys and viceversa. I think in this way it's more comprehensible for clients that don't have to know the internal representation of these data. – SimoV8 Dec 31 '15 at 10:45
  • I guess this is correct for your particular case. I'm just not convinced that passing a label (instead of a key) into `to_internal_value` is the best practice to follow. In general passing key representation from the client would make more sense. Unless this is somehow impossible in your case, therefore your solution is acceptable. +1 ;) – mariodev Dec 31 '15 at 11:46
  • Hi @SimoV8 The answer works, I am just wondering if there is an easier way to achieve this since the answer was posted? – Vikram Bankar Aug 12 '21 at 13:57
1

There's no way other than overriding your serializer. Please take a look here, to see how it can be done.

Community
  • 1
  • 1
mariodev
  • 13,928
  • 3
  • 49
  • 61
  • Hi, thank you, I read it. It works in getting resources but not to create/update a new entity, any idea? – SimoV8 Dec 30 '15 at 19:01
  • Oh you haven't mentioned that.. so maybe you can define the [ChoiceField serializer field](http://www.django-rest-framework.org/api-guide/fields/#choicefield) in your serializer instead.. – mariodev Dec 30 '15 at 19:23
1

previous answers helped me a lot but not worked for me as I use Django version 3 and DRF version 3.11 , so I came up with this:

# models.py
class Ball(models.Model):
    
    class Types(models.TextChoice):
        VOLLYBALL = 'VB', gettext_lazy('VollyBall')
        FOOTBALL = 'FB', gettext_lazy('FootBall')

    type = models.CharField(max_length=2, choices=Types.choices)
# serializers.py

class CustomChoiceField(serializers.ChoiceField):

    def to_representation(self, value):
        if value in ('', None):
            return value

        choice_dict = {str(key): key.label for key in self.choices}
        return choice_dict.get(str(value), value)

    def to_internal_value(self, data):
        if data == '' and self.allow_blank:
            return ''

        try:
            choice_dict = {key.label: str(key) for key in self.choices}
            return choice_dict[str(data)]
        except KeyError:
            self.fail('invalid_choice', input=data)

class BallSerializer(serializers.ModelSerializer):
    type = CustomChoiceField(choices=Book.Types)
    
    class Meta:
        model = Book
        fields = ['type']

YosSaL
  • 667
  • 6
  • 15