Django REST Framework: how to make verbose name of

2019-06-24 01:01发布

问题:

I've got a model with a field tool_class, whose verbose name is class and differs from name:

class Tool(models.Model):
    tool_class = jsonfield.JSONField(verbose_name="class")

The Serializer and ViewSet are just stock HyperlinkedModelSerializer and ModelViewSet.

So, when I POST or PUT data to the server with a key class, it is recognized fine:

'{"class": "..."}

but in the response data it is called tool_class again:

{"tool_class": "..."}

How to make it be called class always?

I can't use the name "class" for the field name, as it is a reserved word in python, but in API it absolutely must be called "class", because the API conforms to a certain open standard, which specifies this word.

Obviously, I cannot say:

class = CharField(source="tool_class")

in my ToolSerializer, because it's a SyntaxError: invalid syntax.


SIMPLE SOLUTION: Guys in another thread suggested a great solution. You can use vars() syntax to circumvent this problem. For instance, I use the following code:

class Tool(Document):
    vars()['class'] = mongoengine.fields.DictField(required=True)

Serializer creates respective field automatically. Ain't we sneaky?

回答1:

You can do this by overriding the metaclass for Serializers. Here is an example of a serializers.py file.

The main magic is this section of the metaclass

# Remap fields (to use class instead of class_)
fields_ = []
for name, field in fields:
    if name.endswith('_'):
        name = name.rstrip('_')
    fields_.append((name, field))

This takes any field you define in the serializer that ends in an underscore (ie. field_) and removes the underscore from the name when it binds the Fields and sets the _declared_fields attribute on the serializer.

from collections import OrderedDict

from rest_framework import serializers
from rest_framework.fields import Field
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES

class MyMeta(serializers.SerializerMetaclass):

    @classmethod
    def _get_declared_fields(cls, bases, attrs):
        fields = [(field_name, attrs.pop(field_name))
                  for field_name, obj in list(attrs.items())
                  if isinstance(obj, Field)]
        fields.sort(key=lambda x: x[1]._creation_counter)

        # If this class is subclassing another Serializer, add that Serializer's
        # fields.  Note that we loop over the bases in *reverse*. This is necessary
        # in order to maintain the correct order of fields.
        for base in reversed(bases):
            if hasattr(base, '_declared_fields'):
                fields = list(base._declared_fields.items()) + fields

        # Remap fields (to use class instead of class_)
        fields_ = []
        for name, field in fields:
            if name.endswith('_'):
                name = name.rstrip('_')
            fields_.append((name, field))

        return OrderedDict(fields_)


class ToolSerializer(serializers.Serializer):

    __metaclass__ = MyMeta

    ...
    class_ = serializers.JSONField(source='tool_class', label='class')

    def create(self, validated_data):
        """
        Create and return a new `Snippet` instance, given the validated data.
        """
        return Snippet.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """
        Update and return an existing `Snippet` instance, given the validated data.
        """
        ...
        instance.class_ = validated_data.get('class', instance.class_)
        instance.save()
        return instance


回答2:

I tried to find a way to have a field called class on the serializer, using some tricks with setattr, but it was getting very intrusive and hacky. The field_name is collected from the field at the time of binding the field to the serializer, and there is no easy place to override the behaviour of the bind.

In the end I decided it would be better and simpler just to let DRF do its thing, and add a post-processing step on the serializer:

class ToolSerializer(ModelSerializer):
    class Meta:
        model = Tool

    def to_representation(self, instance):
        data = super(ToolSerializer, self).to_representation(instance)
        data['class'] = data.pop('tool_class')
        return data

Note that the data structure returned by to_representation is an OrderedDict, and this disturbs the ordering slightly - the renamed key in this mapping will be removed from wherever it was at and pushed to the back.

That is unlikely to be an issue for most use-cases, so you shouldn't bother to address it if not necessary. If you do need to preserve ordering, rebuild a new OrderedDict using a comprehension:

data = OrderedDict(
    ('class' if k == 'tool_class' else k, v) for (k, v) in data.items()
)