36

I'm reading up on Django REST Framework and I have a model that is serialized with getters using the SerializerMethodField().

However, when I POST to this endpoint, I want to be able to set this field as well, but that doesn't work because, as the docs show above, you can't write to a SerializerMethodField. Is there any way in Django REST to have a serializer field that you define a custom getter method for, and a custom setter method?

EDIT: Here's the source of what I'm trying to do. Client has a 1-to-1 relationship with User.

class ClientSerializer(serializers.ModelSerializer):
    email = serializers.SerializerMethodField()

    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",
        )
4
  • stackoverflow.com/questions/18396547/… Commented Nov 11, 2016 at 20:22
  • 2
    Yeah that's kinda what I'm asking about. It says that the you can't really accept data to a serializermethodfield on POST in one of those comments, but I'm asking if there's a way to do that in Django REST that perhaps doesn't use the serializermethodfield Commented Nov 11, 2016 at 20:51
  • 1
    you should post the the source for your usage of the django rest serializer method. Why not just use a normal serializer field? You cannot use method serializer for post requests as it is a read only field Commented Nov 11, 2016 at 21:08
  • Ok I added my code. How can I modify it to accept email in a POST request? Commented Nov 11, 2016 at 21:15

8 Answers 8

23

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}

Sign up to request clarification or add additional context in comments.

3 Comments

I tried your method and it works. Can you explain why super(serializers.SerializerMethodField, self).__init__(**kwargs) instead of super().__init__(**kwargs). I tried using later and it didn't work!
@haccks because the __init__ called will be the one of the parent of SerializerMethodField instead of the parent of ReadWriteSerializerMethodField
Ya. I asked this as a question couple days agao. Git it now. Thanks.
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",
        )

Comments

9

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

1 Comment

can you post an example on how to use it with actual data? For example in the django shell
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}

Comments

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

Comments

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

2 Comments

I definitely could do that, but I was hoping there was a way to handle this use case in Django REST serializer already. It seems odd to separate the logic of serialization and fields from the serializer itself
Is it possible to override the post method of a ViewSet as well?
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.

Comments

-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",
        )

2 Comments

no, that does not work. you cannot have two attributes with the same name on a class in python. in this case the first is overwritten and you get only the EmailField.
With some metaclass magic and elbow grease you could make it work though.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.