[Django]-Django REST Serializer Method Writable Field

10👍

You need to use another type of field:

class ClientSerializer(serializers.ModelSerializer):
    email = serializers.EmailField(source='user.email')

    def create(self, validated_data):
        # DRF will create object {"user": {"email": "inputed_value"}} in validated_date
        email = validated_data.get("user", {}).get('email')

    class Meta:
        model = Client
        fields = (
            "id",
            "email",
        )
👤zymud

20👍

Here is a read/write serializer method field:

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

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

7👍

I tried to use Guilherme Nakayama da Silva and Julio Marins‘s answers to fix my problem with writing to a SerializerMethodField. It worked for reading and updating, but not for creating.

So I created my own WritableSerializerMethodField based on their answers, it works perfectly for reading, creating and writing.

class WritableSerializerMethodField(serializers.SerializerMethodField):
    def __init__(self, **kwargs):
        self.setter_method_name = kwargs.pop('setter_method_name', None)
        self.deserializer_field = kwargs.pop('deserializer_field')

        super().__init__(**kwargs)

        self.read_only = False

    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 get_default(self):
        default = super().get_default()

        return {
            self.field_name: default
        }

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

Then I used this in my serializer

class ProjectSerializer(serializers.ModelSerializer):
    contract_price = WritableSerializerMethodField(deserializer_field=serializers.DecimalField(max_digits=12, decimal_places=2))

    def get_contract_price(self, project):
        return project.contract_price

    def set_contract_price(self, value):
        return value
👤Dolan

4👍

In my case, I needed the logic inside my get_* method and couldn’t fetch the value using the source attribute. So I came up with this field.

class WritableSerializerMethodField(serializers.SerializerMethodField):
    def __init__(self, method_name=None, **kwargs):
        super().__init__(**kwargs)

        self.read_only = False

    def get_default(self):
        default = super().get_default()

        return {
            self.field_name: default
        }

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

1👍

You can override the save() method on the serializer and use self.initial_data. You’ll then need to do the validation on that field yourself though.

class MySerializer(serializers.ModelSerializer):

    magic_field = serializers.SerializerMethodField()

    def get_magic_field(self, instance):
        return instance.get_magic_value()

    def save(self, **kwargs):

        super().save(**kwargs)  # This creates/updates `self.instance`

        if 'magic_field' in self.initial_data:
            self.instance.update_magic_value(self.initial_data['magic_field'])

        return self.instance

0👍

Why not just create the Client in the view instead?

def post(self, request, *args, **kwargs):
    data = {
        'email': request.data.get('email'),
    }

    serializer = ClientSerializer(data=data)
    if serializer.is_valid():
        email = serializer.data.get('email')
        client = Client.objects.create(email=email)
        # do other stuff

0👍

I had the same issue and came up with the solution below.

Note that I really needed to use a SerializerMethodField in my serializer, as I needed to populate a field based on request.user and certain permissions, which was too complex for a SerializerField, or other solutions proposed in other answers.

The solution was to “hijack” the perform_update of the API View, and perform specific writes at that point (in my case, using another Serializer on top of the normal one). I only needed to do this with the update, but you may need to do it with perform_create, if this is your use case.

It goes like this:

    def perform_update(self, serializer):
        if 'myField' in self.request.data and isinstance(self.request.data['myField'], bool):
        if self.request.user == serializer.instance.owner:
            serializer.instance.myField = self.request.data['myField']
        else:
            # we toggle myField in OtherClass
            try:
                other = models.OtherClass.objects.get(...)
            except models. OtherClass.DoesNotExist:
                return Response("You don't sufficient permissions to run this action.", status=status.HTTP_401_UNAUTHORIZED)
            except models.OtherClass.MultipleObjectsReturned:  # should never happen...
                return Response("Internal Error: too many instances.", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            else:
                data = {
                    'myField': self.request.data['myField']
                    ... # filled up with OtherClass params
                }
                otherSerializer = serializers.OtherClassSerializer(other, data=data)
                if otherSerializer.is_valid():
                    otherSerializer.save()
    serializer.save()  # takes care of all the non-read-only fields 

I have to admit that it is not ideal as per the MVC pattern, but it works.

-4👍

You can repeat email field, and it works, but it may make confused

class ClientSerializer(serializers.ModelSerializer):
    email = serializers.SerializerMethodField()
    email = serializers.EmailField(required=False)

    def create(self, validated_data):
        email = validated_data.get("email", None) # This doesn't work because email isn't passed into validated_data because it's a readonly field
        # create the client and associated user here


    def get_email(self, obj):
        return obj.user.email

    class Meta:
        model = Client
        fields = (
            "id",
            "email",
        )

Leave a comment