[Django]-Django Rest Framework with ChoiceField

60👍

An update for this thread, in the latest versions of DRF there is actually a ChoiceField.

So all you need to do if you want to return the display_name is to subclass ChoiceField to_representation method like this:

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()

class ChoiceField(serializers.ChoiceField):

    def to_representation(self, obj):
        if obj == '' and self.allow_blank:
            return obj
        return self._choices[obj]

    def to_internal_value(self, data):
        # To support inserts with the value
        if data == '' and self.allow_blank:
            return ''

        for key, val in self._choices.items():
            if val == data:
                return key
        self.fail('invalid_choice', input=data)


class UserSerializer(serializers.ModelSerializer):
    gender = ChoiceField(choices=User.GENDER_CHOICES)

    class Meta:
        model = User

So there is no need to change the __init__ method or add any additional package.

204👍

Django provides the Model.get_FOO_display method to get the “human-readable” value of a field:

class UserSerializer(serializers.ModelSerializer):
    gender = serializers.SerializerMethodField()

    class Meta:
        model = User

    def get_gender(self,obj):
        return obj.get_gender_display()

for the latest DRF (3.6.3) – easiest method is:

gender = serializers.CharField(source='get_gender_display')
👤levi

29👍

I suggest to use django-models-utils with a custom DRF serializer field

Code becomes:

# models.py
from model_utils import Choices

class User(AbstractUser):
    GENDER = Choices(
       ('M', 'Male'),
       ('F', 'Female'),
    )

    gender = models.CharField(max_length=1, choices=GENDER, default=GENDER.M)


# serializers.py 
from rest_framework import serializers

class ChoicesField(serializers.Field):
    def __init__(self, choices, **kwargs):
        self._choices = choices
        super(ChoicesField, self).__init__(**kwargs)

    def to_representation(self, obj):
        return self._choices[obj]

    def to_internal_value(self, data):
        return getattr(self._choices, data)

class UserSerializer(serializers.ModelSerializer):
    gender = ChoicesField(choices=User.GENDER)

    class Meta:
        model = User

# viewsets.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

16👍

Probalbly you need something like this somewhere in your util.py and import in whichever serializers ChoiceFields are involved.

class ChoicesField(serializers.Field):
    """Custom ChoiceField serializer field."""

    def __init__(self, choices, **kwargs):
        """init."""
        self._choices = OrderedDict(choices)
        super(ChoicesField, self).__init__(**kwargs)

    def to_representation(self, obj):
        """Used while retrieving value for the field."""
        return self._choices[obj]

    def to_internal_value(self, data):
        """Used while storing value for the field."""
        for i in self._choices:
            if self._choices[i] == data:
                return i
        raise serializers.ValidationError("Acceptable values are {0}.".format(list(self._choices.values())))

10👍

Since DRF 3.1 there is new API called customizing field mapping. I used it to change default ChoiceField mapping to ChoiceDisplayField:

import six
from rest_framework.fields import ChoiceField


class ChoiceDisplayField(ChoiceField):
    def __init__(self, *args, **kwargs):
        super(ChoiceDisplayField, self).__init__(*args, **kwargs)
        self.choice_strings_to_display = {
            six.text_type(key): value for key, value in self.choices.items()
        }

    def to_representation(self, value):
        if value in ('', None):
            return value
        return {
            'value': self.choice_strings_to_values.get(six.text_type(value), value),
            'display': self.choice_strings_to_display.get(six.text_type(value), value),
        }

class DefaultModelSerializer(serializers.ModelSerializer):
    serializer_choice_field = ChoiceDisplayField

If You use DefaultModelSerializer:

class UserSerializer(DefaultModelSerializer):    
    class Meta:
        model = User
        fields = ('id', 'gender')

You will get something like:

...

"id": 1,
"gender": {
    "display": "Male",
    "value": "M"
},
...
👤lechup

9👍

The following solution works with any field with choices, with no need to specify in the serializer a custom method for each:

from rest_framework import serializers

class ChoicesSerializerField(serializers.SerializerMethodField):
    """
    A read-only field that return the representation of a model field with choices.
    """

    def to_representation(self, value):
        # sample: 'get_XXXX_display'
        method_name = 'get_{field_name}_display'.format(field_name=self.field_name)
        # retrieve instance method
        method = getattr(value, method_name)
        # finally use instance method to return result of get_XXXX_display()
        return method()

Example:

given:

class Person(models.Model):
    ...
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)

use:

class PersonSerializer(serializers.ModelSerializer):
    ...
    gender = ChoicesSerializerField()

to receive:

{
    ...
    'gender': 'Male'
}

instead of:

{
    ...
    'gender': 'M'
}

6👍

I’m late to the game, but I was facing a similar situation and reached a different solution.

As I tried the previous solutions, I began to wonder whether it made sense for a GET request to return the field’s display name but expect the user to send me the field’s value on a PUT request (because my app is translated to many languages, allowing the user to input the display value would be a recipe for disaster).

I would always expect the output for a choice in the API to match the input – regardless of the business requirements (as these can be prone to change)

So the solution I came up with (on DRF 3.11 btw) was to create a second, read only field, just for the display value.

class UserSerializer(serializers.ModelSerializer):
    gender_display_value = serializers.CharField(
        source='get_gender_display', read_only=True
    )

    class Meta:
        model = User
        fields = (
            "username",
            "email",
            "first_name",
            "last_name",
            "gender",
            "gender_display_value",
        )

That way I keep a consistent API’s signature and don’t have to override DRF’s fields and risk mixing up Django’s built-in model validation with DRF’s validation.

The output will be:

{
  'username': 'newtestuser',
  'email': 'newuser@email.com',
  'first_name': 'first',
  'last_name': 'last',
  'gender': 'M',
  'gender_display_value': 'Male'
}

0👍

I found soup boy‘s approach to be the best. Though I’d suggest to inherit from serializers.ChoiceField rather than serializers.Field. This way you only need to override to_representation method and the rest works like a regular ChoiceField.

class DisplayChoiceField(serializers.ChoiceField):

    def __init__(self, *args, **kwargs):
        choices = kwargs.get('choices')
        self._choices = OrderedDict(choices)
        super(DisplayChoiceField, self).__init__(*args, **kwargs)

    def to_representation(self, obj):
        """Used while retrieving value for the field."""
        return self._choices[obj]

0👍

I prefer the answer by @nicolaspanel to keep the field writeable. If you use this definition instead of his ChoiceField, you take advantage of any/all of the infrastructure in the built-in ChoiceField while mapping the choices from str => int:

class MappedChoiceField(serializers.ChoiceField):

    @serializers.ChoiceField.choices.setter
    def choices(self, choices):
        self.grouped_choices = fields.to_choices_dict(choices)
        self._choices = fields.flatten_choices_dict(self.grouped_choices)
        # in py2 use `iteritems` or `six.iteritems`
        self.choice_strings_to_values = {v: k for k, v in self._choices.items()}

The @property override is “ugly” but my goal is always to change as little of the core as possible (to maximize forward compatibility).

P.S. if you want to allow_blank, there’s a bug in DRF. The simplest workaround is to add the following to MappedChoiceField:

def validate_empty_values(self, data):
    if data == '':
        if self.allow_blank:
            return (True, None)
    # for py2 make the super() explicit
    return super().validate_empty_values(data)

P.P.S. If you have a bunch of choice fields that all need to be mapped this, way take advantage of the feature noted by @lechup and add the following to your ModelSerializer (not its Meta):

serializer_choice_field = MappedChoiceField

Leave a comment