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?
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
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()
)