Django-rest-framework: SerializerMethodField also supporting a setter

Created on 20 Mar 2015  路  7Comments  路  Source: encode/django-rest-framework

A good enhancement (in my opinion) will be a possibility to also have a setter in the SerializerMethodField.

So of only having get_<field_name> an option to have set_<field_name>

Proposed solution

Set it as a possible kwarg as in setter="set_something". When the SerializerMethodField's to_internal_value is fired it can check if this setter kwarg is set up and fire this function:

something = SerializerMethodField(setter="set_something")


def set_something(self, value, obj):
   obj.value = value
   return obj

Just an enhancement I thought would be nice to have, any thoughts?

Most helpful comment

Does not seem cleaner to me, especially, because I had to override __init__ without calling direct parent method (this is because kwargs['read_only'] = True is used instead of more compatible kwargs.setdefault('read_only', True) - developers who know what they are doing could benefit from it).

class WritableSerializerMethodField(serializers.SerializerMethodField):

    def __init__(self, method_name=None, **kwargs):
        self.method_name = method_name
        self.setter_method_name = kwargs.pop('setter_method_name', None)
        self.deserializer_field = kwargs.pop('deserializer_field')

        kwargs['source'] = '*'
        super(serializers.SerializerMethodField, self).__init__(**kwargs)

    def bind(self, field_name, parent):
        retval = super().bind(field_name, parent)
        if not self.setter_method_name:
            self.setter_method_name = f'set_{field_name}'

        return retval

    def to_internal_value(self, data):
        value = self.deserializer_field.to_internal_value(data)
        method = getattr(self.parent, self.setter_method_name)
        method(value)
        return {}

Usage example:

class UserJobSerializer(CustomModelSerializer):

    status = WritableSerializerMethodField(
        deserializer_field=serializers.ChoiceField(JOB_STATUS_CHOICES))

    def get_status(self, obj):
        return obj.get_attr_for_user('status', self.get_current_user())

    def set_status(self, value):
        self.instance.set_status_for_user(value, self.get_current_user())

All 7 comments

SerializerMethodField is a read only field and therefore it doesn't even have a to_internal_value method.

@maryokhin I get that, maybe my explanation is not very clear, what I would like to achieve is that there is also a setter available inside the Serializer.

It doesn't have to be the SerializerMethodField itself, maybe a new Field that extends for SerializerMethodField and makes it possible to have a set_<field_name>

Ah, okay, I didn't understand the whole picture then.

I would say that if you need both input and output for a field then the intent is cleaner to implement your own field with to_internal_value and to_representation, but that's just my opinion of course, maybe the setter approach does make sense in some use-cases.

Seconding @maryokhin's comment above.

"I would say that if you need both input and output for a field then the intent is cleaner to implement your own field with to_internal_value and to_representation" - or implement your own DRF :). I also need this feature. I can use setter (point source to it) in most of the cases, but when I need data from serializer context (for example self.context['request'].user) it is not longer enough. Having set_<field_name> would be really nice.

Does not seem cleaner to me, especially, because I had to override __init__ without calling direct parent method (this is because kwargs['read_only'] = True is used instead of more compatible kwargs.setdefault('read_only', True) - developers who know what they are doing could benefit from it).

class WritableSerializerMethodField(serializers.SerializerMethodField):

    def __init__(self, method_name=None, **kwargs):
        self.method_name = method_name
        self.setter_method_name = kwargs.pop('setter_method_name', None)
        self.deserializer_field = kwargs.pop('deserializer_field')

        kwargs['source'] = '*'
        super(serializers.SerializerMethodField, self).__init__(**kwargs)

    def bind(self, field_name, parent):
        retval = super().bind(field_name, parent)
        if not self.setter_method_name:
            self.setter_method_name = f'set_{field_name}'

        return retval

    def to_internal_value(self, data):
        value = self.deserializer_field.to_internal_value(data)
        method = getattr(self.parent, self.setter_method_name)
        method(value)
        return {}

Usage example:

class UserJobSerializer(CustomModelSerializer):

    status = WritableSerializerMethodField(
        deserializer_field=serializers.ChoiceField(JOB_STATUS_CHOICES))

    def get_status(self, obj):
        return obj.get_attr_for_user('status', self.get_current_user())

    def set_status(self, value):
        self.instance.set_status_for_user(value, self.get_current_user())

This is another implementation:

class ReadWriteSerializerMethodField(serializers.Field):
    def __init__(self, method_name=None, **kwargs):
        self.method_name = method_name
        kwargs['source'] = '*'
        #kwargs['read_only'] = True
        super(ReadWriteSerializerMethodField, self).__init__(**kwargs)

    def bind(self, field_name, parent):
        self.field_name = field_name
        # In order to enforce a consistent style, we error if a redundant
        # 'method_name' argument has been used. For example:
        # my_field = serializer.SerializerMethodField(method_name='get_my_field')
        default_method_name = 'get_{field_name}'.format(field_name=field_name)
        assert self.method_name != default_method_name, (
            "It is redundant to specify `%s` on SerializerMethodField '%s' in "
            "serializer '%s', because it is the same as the default method name. "
            "Remove the `method_name` argument." %
            (self.method_name, field_name, parent.__class__.__name__)
        )

        # The method name should default to `get_{field_name}`.
        if self.method_name is None:
            self.method_name = default_method_name

        super(ReadWriteSerializerMethodField, self).bind(field_name, parent)

    def to_representation(self, value):
        method = getattr(self.parent, self.method_name)
        return method(value)

    def to_internal_value(self, data):
        return { self.field_name: data }

Was this page helpful?
0 / 5 - 0 ratings