Reputation: 10182
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': '[email protected]',
'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.
Upvotes: 132
Views: 133301
Reputation: 696
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': '[email protected]',
'first_name': 'first',
'last_name': 'last',
'gender': 'M',
'gender_display_value': 'Male'
}
Upvotes: 7
Reputation: 3150
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"
},
...
Upvotes: 10
Reputation: 1553
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.
Upvotes: 63
Reputation: 1099
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
Upvotes: 0
Reputation: 22697
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')
Upvotes: 205
Reputation: 73
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]
Upvotes: 0
Reputation: 5849
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'
}
Upvotes: 10
Reputation: 2678
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())))
Upvotes: 17
Reputation: 973
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
Upvotes: 31