Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

List Serializer with dynamic fields in Django Rest Framework

I'm trying to add fields dynamically to a serializer of Django Rest Framework, by overwriting the __init__ method. The approach is similar to the one described here: http://masnun.com/2015/10/21/django-rest-framework-dynamic-fields-in-serializers.html

The reason I'm doing this is, because I want to change the field type dynamically to a one, determined by the property type_ of the instance to be serialized. This works pretty well, if I serialize one instance at a time:

from rest_framework import serializers
from rest_framework.fields import empty


class VariableDetails:
    def __init__(self, name, type_, value):
        self.name = name
        self.type_ = type_
        self.value = value


class VariableDetailSerializer(serializers.Serializer):
    TYPE_FIELD_MAP = {
        'string': serializers.CharField,
        'integer': serializers.IntegerField,
        'float': serializers.FloatField,
    }

    name = serializers.CharField()
    type_ = serializers.CharField()

    def __init__(self, instance=None, data=empty, **kwargs):
        # this is where the magic happens
        super().__init__(instance, data, **kwargs)
        if instance is not None:
            field_type = self.TYPE_FIELD_MAP[instance.type_]
            self.fields['value'] = field_type()

string_details = VariableDetails('character value', 'string', 'hello world')
integer_details = VariableDetails('integer value', 'integer', 123)

print(VariableDetailSerializer(string_details).data)
# {'name': 'character value', 'type_': 'string', 'value': 'hello world'}

print(VariableDetailSerializer(integer_details).data)
# {'name': 'integer value', 'type_': 'integer', 'value': 123}

If I want to serialize multiple instances of VariableDetails that are related to a parent instance (calling it Parent for the example), the value field is missing:

class Parent:
    def __init__(self, variable_details):
        self.variable_details = variable_details


class ParentSerializer(serializers.Serializer):
    variable_details = VariableDetailSerializer(many=True)


parent = Parent(variable_details=[string_details, integer_details])
print(ParentSerializer(parent).data)
# {
#     'variable_details': [
#         {
#             'name': 'character_value',
#             'type_': 'string'
#             # value is missing
#         },
#         {
#             'name': 'integer_value',
#             'type_': 'integer'
#         },
#     ]
# }

Apparently VariableDetailSerializer.__init__ is only called during the creation of the ParentSerializer and once when a new instance of the ParentSerializer is initialized. In both cases instance is None. Hence, it is not called for each of the VariableDetails.

Does anybody know how to add fields dynamically to a Serializer that is also serializing list instances?

To facilitate running the code, I created a gist: https://gist.github.com/larsrinn/861f8d50bf5bb0626d73321b546d8cb3 The code should be copy&pastable into a python repl, if Django Rest Framework is installed. However, you don't need to create a Django Project

like image 648
lmr2391 Avatar asked Oct 15 '25 13:10

lmr2391


1 Answers

Because VariableDetailSerializer is nested in ParentSerializer it behaves a like a field itself. In this scenario you can override the to_representation() method to do the dynamic type switching.

def to_representation(self, instance):
    field_type = self.TYPE_FIELD_MAP[instance.type_]
    self.fields['value'] = field_type()
    try:
        del self._readable_fields  # Clear the cache
    except AttributeError:
        pass
    return super().to_representation(instance)

The main catch here is that the serializer caches the fields because it does not expect them to change between instances. Therefore it is necessary to clear the cache with the del self._readable_fields.

When I add the above to VariableDetailSerializer and run your example, I get:

{
    'variable_details': [
        OrderedDict([('name', u'character value'), 
                     ('type_', u'string'), 
                     ('value', u'hello world')]), 
        OrderedDict([('name', u'integer value'), 
                     ('type_', u'integer'), 
                     ('value', 123)])
    ]
}
like image 175
Will Keeling Avatar answered Oct 18 '25 15:10

Will Keeling