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')
- [Django]-Allowing only super user login
- [Django]-How to get superuser details in Django?
- [Django]-STATIC_ROOT vs STATIC_URL in Django
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
- [Django]-How to add multiple objects to ManyToMany relationship at once in Django ?
- [Django]-Django-reversion and related model
- [Django]-How to update an existing Conda environment with a .yml file
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())))
- [Django]-Django testing: Test the initial value of a form field
- [Django]-Can't connect to local MySQL server through socket '/tmp/mysql.sock
- [Django]-Can you give a Django app a verbose name for use throughout the admin?
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"
},
...
- [Django]-Django create userprofile if does not exist
- [Django]-Sending HTML email in django
- [Django]-Django admin: how to sort by one of the custom list_display fields that has no database field
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'
}
- [Django]-How to resize the new uploaded images using PIL before saving?
- [Django]-Pagination in Django-Rest-Framework using API-View
- [Django]-Django REST Framework – 405 METHOD NOT ALLOWED using SimpleRouter
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'
}
- [Django]-Unittest Django: Mock external API, what is proper way?
- [Django]-Is there a way to loop over two lists simultaneously in django?
- [Django]-Django: Multiple forms possible when using FormView?
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]
- [Django]-Django Rest Framework – Could not resolve URL for hyperlinked relationship using view name "user-detail"
- [Django]-How to move a model between two Django apps (Django 1.7)
- [Django]-Rendering a value as text instead of field inside a Django Form
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
- [Django]-How to set a value of a variable inside a template code?
- [Django]-What is a django.utils.functional.__proxy__ object and what it helps with?
- [Django]-A field with precision 10, scale 2 must round to an absolute value less than 10^8