Django-rest-framework: ChoiceField should serialize the human-readable form in to_native

Created on 13 Aug 2014  路  8Comments  路  Source: encode/django-rest-framework

Currently the ChoiceField serializes to_native through its base class which causes it to return the first of the choice tuple. This is what Django does. There should be an option to use the "get_FOO_display"-function type of behaviour (https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_FOO_display) to get ChoiceFields to serialize the human-readable form in native.

Most helpful comment

I use django-enumfields with a custom DRF field for this:

models.py

from enum import Enum
from django.db import models
from enumfields import EnumIntegerField

class GeoModel(models.Model):

    class LocationType(Enum):
        ROOFTOP = 1
        RANGE_INTERPOLATED = 2
        GEOMETRIC_CENTER = 3
        APPROXIMATE = 4
        UNRESOLVED = 5

    location_type = EnumIntegerField(
        enum=LocationType,
        default=LocationType.UNRESOLVED
    )
    ...

fields.py

from rest_framework import serializers


class EnumField(serializers.ChoiceField):
    def __init__(self, enum, **kwargs):
        self.enum = enum
        kwargs['choices'] = [(e.name, e.name) for e in enum]
        super(EnumField, self).__init__(**kwargs)

    def to_representation(self, obj):
        return obj.name

    def to_internal_value(self, data):
        try:
            return self.enum[data]
        except KeyError:
            self.fail('invalid_choice', input=data)

serializers.py

from rest_framework import serializers
from . import fields
from . import models

class GeoModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.GeoModel

    location_type = fields.EnumField(enum=models.GeoModel.LocationType)

All 8 comments

Could you expand on this from the user point-of-view.

Ie: Give a simple ChoiceField example, then show:

  • What it current accepts as API input.
  • What it currently returns as API output.
  • What it currently displays in the browsable API.

Then present what you'd instead like the behaviour to be in each of those three cases?

That'd be tremendously helpful! :)

I ended up writing a very simple subclass of ChoiceField that outputs what I needed.

class NewChoiceField(ChoiceField):
    def field_to_native(self, obj, field_name):
        return getattr(obj,'get_'+field_name+'_display')()

To elaborate what I meant with my previous one:

class FoodModel(models.Model):
    food_choices = (('pasta','Delicious Pasta'), ('pizza', 'Big pizza'))
    food = CharField(choices=food_choices)

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = FoodModel
        fields = ("food")

Current output:
{
    'food': 'pasta'
}

What I wanted:
{
    'food': 'Delicious Pasta'
}

Don't know how it could be included as an option perhaps to display either the actual value or the human-readable name (https://docs.djangoproject.com/en/dev/ref/models/fields/#choices)

Unlike with purely HTML based forms where the presentation may not match what's stored at the data layer, I would always expect the output for a choice in the API to match the input (at least for a standard ChoiceField).

I'd be willing to look at changing the behaviour of what is presented in the browsable API (ie the display of the dropdown), but I'm not convinced by allowing the API input and API output to differ. (Though further examples of custom field types in the docs wouldn't be a bad idea?)

Closing this off on the basis of... "I'd be willing to look at changing the behaviour of what is presented in the browsable API (ie the display of the dropdown), but I'm not convinced by allowing the API input and API output to differ."

Willing to consider alternate issues/pull requests along those lines.

I would also prefer the readable form of choices. Giving an integer back makes me go "wtf does that mean"?

Matching input vs output is irrelevant; only the business requirement should be considered.

At least add an argument to the ChoiceField to overwrite label over key for the return value. If you want to return the key value then the value object should be returned as a nested object with its properties, but single field representations can just as well be the label thereof.

Pull requests to set this as an option would be considered. Not particularly motivated to push this on the core team otherwise.

I use django-enumfields with a custom DRF field for this:

models.py

from enum import Enum
from django.db import models
from enumfields import EnumIntegerField

class GeoModel(models.Model):

    class LocationType(Enum):
        ROOFTOP = 1
        RANGE_INTERPOLATED = 2
        GEOMETRIC_CENTER = 3
        APPROXIMATE = 4
        UNRESOLVED = 5

    location_type = EnumIntegerField(
        enum=LocationType,
        default=LocationType.UNRESOLVED
    )
    ...

fields.py

from rest_framework import serializers


class EnumField(serializers.ChoiceField):
    def __init__(self, enum, **kwargs):
        self.enum = enum
        kwargs['choices'] = [(e.name, e.name) for e in enum]
        super(EnumField, self).__init__(**kwargs)

    def to_representation(self, obj):
        return obj.name

    def to_internal_value(self, data):
        try:
            return self.enum[data]
        except KeyError:
            self.fail('invalid_choice', input=data)

serializers.py

from rest_framework import serializers
from . import fields
from . import models

class GeoModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.GeoModel

    location_type = fields.EnumField(enum=models.GeoModel.LocationType)

Thanks man a easy way to solve my problem...

Was this page helpful?
0 / 5 - 0 ratings