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 simplifi
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
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 is 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"
},
...
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'
}
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())))
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]
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')