MLu
MLu

Reputation: 1380

How to deserialize POST request using a different Serializer

I'm trying to handle POST requests in my API endpoint using a different serializer than the one derived from Model and used for GET or PUT requests. The format of the POSTed messages is different from the Model and from GET/PUT and must be pre-processed before storing to the database.

As a demo of my problem I made a very simple model and the corresponding API view and serializer:

class Message(models.Model): 
    message = models.CharField(max_length = 500)

class MessageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Message
        fields = ('message',)

class MessageViewSet(viewsets.ModelViewSet):
    queryset = Message.objects.all().order_by('-pk')
    serializer_class = MessageSerializer

That works well. Then I tried to override the MessageViewSet.create() to handle POST requests differently.

class MessageSerializer_FromTo(serializers.Serializer):
    sender = serializers.EmailField()
    recipient = serializers.EmailField()

    def create(self, validated_data):
        message = "Message from <{sender}> to <{recipient}>".format(**validated_data)
        return Message(message)

class MessageViewSet(viewsets.ModelViewSet):
    queryset = Message.objects.all().order_by('-pk')
    serializer_class = MessageSerializer

    # Handle POST requests differently
    def create(self, request, format=None):
        message = MessageSerializer_FromTo(data = request.data)
        if message.is_valid():
            message.save()
            return Response(message.data, status=status.HTTP_201_CREATED)
        return Response(message.errors, status=status.HTTP_400_BAD_REQUEST)

Essentially I want to pass this JSON to POST /api/messages/

{"sender": "[email protected]", "recipient": "[email protected]"}

GET /api/messages/1/ should return

{"message": "Message from <[email protected]> to <[email protected]>"}

However the POST fails with this message:

Internal Server Error: /api/messages/
Traceback (most recent call last):
  File ".../rest_framework/fields.py", line 441, in get_attribute
    return get_attribute(instance, self.source_attrs)
  File ".../rest_framework/fields.py", line 100, in get_attribute
    instance = getattr(instance, attr)
AttributeError: 'Message' object has no attribute 'sender'


During handling of the above exception, another exception occurred:
[...]
AttributeError: Got AttributeError when attempting to get a value for field `sender` on serializer `MessageSerializer_FromTo`.
The serializer field might be named incorrectly and not match any attribute or key on the `Message` instance.
Original exception text was: 'Message' object has no attribute 'sender'.
[26/Feb/2018 05:09:08] "POST /api/messages/ HTTP/1.1" 500 19059

This is just to demonstrate the problem obviously, I'm doing more complex things in my POST handler, but the error is like this.

Any idea how to achieve what I need? I.e. accept POST fields that are completely different from the Model fields?

Thanks!

UPDATE: The complete code is here: https://github.com/mludvig/drf-demo

Upvotes: 2

Views: 4049

Answers (3)

hugobessa
hugobessa

Reputation: 546

To use different serializers depending on if the serializer will deserialize the data from the request or serialize it for the response, you can use drf-rw-serializers.

It has generic classes to help you build views like the Django REST Framework ones, but, instead of using only one serializer_class, they use a read_serializer_class and a write_serializer_class.

You can read more instruction about installation and usage in the documentation.

Upvotes: 2

luc
luc

Reputation: 43096

You can get a different serializer depending on the method type by overriding the get_serializer_class method of you viewset

class MessageViewSet(viewsets.ModelViewSet):
    model = Message

    def get_serializer_class(self):
        serializers_class_map = {
            'default': DefaultSerializer,
            'create': CreateMessageSerializer,
        }
        return serializers_class_map.get(self.action, serializers_class_map['default'])

You can also overrides the to_representation method of your CreateMessageSerializer to get a different output

class CreateMessageSerializer(serializer.ModelSerializer):

    class Meta:
        model = Message
        fields = (
            'sender', 'recipient'
        )

    def to_representation(self, instance):
        return {
            'message': "Message from <{0.sender}> to <{0.recipient}>".format(instance)
        }

Upvotes: 0

Aneesh R S
Aneesh R S

Reputation: 3827

problem is with in your serializer. You are just passing model class Message(message) as the output of create function instead of Message object

class MessageSerializer_FromTo(serializers.Serializer):
    sender = serializers.EmailField(write_only=True)
    recipient = serializers.EmailField(write_only=True)
    message = serializers.CharField(read_only=True, max_length = 500)   

    def create(self, validated_data):
        message = "Message from <{sender}> to <{recipient}>".format(**validated_data)
        return Message.objects.create(message=message)

Upvotes: 3

Related Questions