24
votes

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",
        )
8
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 serializermethodfieldNick
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 fieldDap
Ok I added my code. How can I modify it to accept email in a POST request?Nick

8 Answers

6
votes

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

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}

2
votes

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}
2
votes

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
votes

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
votes

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
votes

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
votes

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