129

I have a few fields in my user model that are choice fields and am trying to figure out how to best implement that into Django Rest Framework.

Below is some simplified code to show what I'm doing.

# models.py
class User(AbstractUser):
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )

    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)


# serializers.py 
class UserSerializer(serializers.ModelSerializer):
    gender = serializers.CharField(source='get_gender_display')

    class Meta:
        model = User


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

Essentially what I'm trying to do is to have the get/post/put methods use the display value of the choice field instead of the code, looking something like the below JSON.

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

How would I go about doing that? The above code does not work. Before I had something like this working for GET, but for POST/PUT it was giving me errors. I'm looking for general advice on how to do this, it seems like it would be something common, but I can't find examples. Either that or I'm doing something terribly wrong.

awwester
  • 9,623
  • 13
  • 45
  • 72
  • Did you try http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield – DAKZH Mar 09 '15 at 20:29
  • https://stackoverflow.com/questions/64619906/serializers-multiple-models-using-viewset loicgasser please help me on this – Adnan Rizwee Oct 31 '20 at 09:03

9 Answers9

200

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')
TangoAlee
  • 1,260
  • 2
  • 13
  • 34
levi
  • 22,001
  • 7
  • 73
  • 74
  • 7
    What if you want the display strings of all available choices? – Håken Lid Nov 22 '15 at 23:23
  • @HåkenLid you need to call one by one in a new method. – levi Nov 24 '15 at 15:24
  • 7
    Turns out that rest-framework exposes the choices if you use the http options method on a ModelViewSet, so I didn't need to customize the serializer. – Håken Lid Nov 24 '15 at 21:54
  • @levi what can be my get_gender_display() method be here ? How to return "Male" instead of "M" – Kishan Mehta May 14 '16 at 11:21
  • 1
    @kishan using `get_render_display` you will get `Male`, if you access to the attribute itself `obj.gender`, you will get `M` – levi May 14 '16 at 16:10
  • 6
    as I use drf v3.6.3, `gender = serializers.CharField(source='get_gender_display')` is works well. – dalang Oct 24 '17 at 10:17
  • @pawelFurmaniak you need to return both gender and get_gender_display from your serializer, `gender` for writing and `get_gender_display` for displaying to user. – Vaibhav Vishal Oct 17 '18 at 14:24
  • 1
    @g0g0 it has nothing to do about POST request, I think what you mean is for update/creating object using the serializer, you need to re-write you `validated` or `create` method. – levi Jun 09 '19 at 01:28
  • 3
    You can just do `fuel = serializers.CharField(source='get_fuel_display', read_only=True)` to only display the human readable name for `GET` requests. `POST` requests will still work with the code (on `ModelSerializer`). – Pierre Monico Jun 26 '19 at 07:25
  • What if i want to use with `serializers.Serializer` – TarangP Jun 02 '22 at 05:11
  • Funny that it does not answers the question and the OP mentioned this approach on it's question but yet it has the most votes... – FooBar Jan 09 '23 at 03:36
57

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.

loicgasser
  • 1,403
  • 12
  • 17
  • should that be `return self.GENDER_CHOICES[obj]` in OPs case? and is this preferable to Django's `Model.get_FOO_display`? – Harry Moreno Sep 10 '19 at 03:18
  • You can use DRF's SerializerMethodField http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield if your prefer doing that at the serializer level. I would avoid mixing django built-in validation at the model level with DRF's validation. – loicgasser Sep 11 '19 at 20:25
  • When @kishan-mehta answer did not work for me. This one did – Robert Johnstone Jul 27 '20 at 10:46
  • For me it worked either way, but the advantage of this approach is that `ModelViewSet` default OPTION request will still deliver an array of all possible choices for the field which isn't the case when using `get_FOO_display` – denise Jun 06 '22 at 14:50
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
nicolaspanel
  • 943
  • 11
  • 21
  • 4
    this question was answered a long time ago with a much simpler built-in solution. – awwester Jan 13 '16 at 19:21
  • 11
    2 differences: 1)ChoicesField can be reused and 2)it supports editions for the field "gender" that is not read-only anymore – nicolaspanel Jan 13 '16 at 19:30
  • Given that the values for the choices are themselves so short I would just use `GENDER = Choices('Male', 'Female')` and `default=GENDER.Male`, as that bypasses the need to create a custom serializer field. – ergusto Jan 25 '17 at 22:31
  • 7
    I was looking for a way to read and write a choices field via api and this answer nailed it. The accepted answer doesn't show how to update a choices field, this one does. – JLugao Jun 20 '17 at 19:01
  • Only answer that actually supports writing properly! – yspreen Nov 14 '18 at 01:05
  • FWIW Django Rest Framework's [`serializers.ChoiceField`](https://www.django-rest-framework.org/api-guide/fields/#choicefield) is compatible with django-models-utils' `Choices`, so you can just do `gender = serializers.ChoiceField (choices=User.GENDER)` – Jmb Mar 08 '19 at 13:15
  • Not working if we need this model in the "depth" of another serializer. – Muhammad Zahid Sep 04 '20 at 14:08
15

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())))
Kishan Mehta
  • 2,598
  • 5
  • 39
  • 61
  • 2
    I prefer this answer because this can allow users input eihter choice key or value. By simply changing `if self._choices[i] == data:` into `if i == data or self._choices[i] == data:`. While inheriting from ChoiceField doesn't need to override `to_internal_value()` but will only accept choice key. – C.K. Dec 30 '18 at 21:48
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"
},
...
ChrisRob
  • 1,515
  • 2
  • 18
  • 30
lechup
  • 3,031
  • 27
  • 26
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'
}
Jamie Counsell
  • 7,730
  • 6
  • 46
  • 81
Mario Orlandi
  • 5,629
  • 26
  • 29
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'
}
Original BBQ Sauce
  • 527
  • 1
  • 11
  • 24
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]
rajat404
  • 73
  • 1
  • 7
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
claytond
  • 1,061
  • 9
  • 22