Invalid data. Expected a dictionary, but got str error with serializer field in Django Rest Framework

Anuj TBE picture Anuj TBE · Oct 18, 2018 · Viewed 11k times · Source

I'm using Django 2.x and Django REST Framework.

I have two models like

class Contact(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.PROTECT)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100, blank=True, null=True)
    modified = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now_add=True)

class AmountGiven(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
    amount = models.FloatField(help_text='Amount given to the contact')
    given_date = models.DateField(default=timezone.now)
    created = models.DateTimeField(auto_now=True)

the serializer.py the file has serializers defined as

class ContactSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = Contact
        fields = ('id', 'first_name', 'last_name', 'created', 'modified')

class AmountGivenSerializer(serializers.ModelSerializer):
    contact = ContactSerializer()

    class Meta:
        model = AmountGiven
        depth = 1
        fields = (
            'id', 'contact', 'amount', 'given_date', 'created'
        )

views.py

class AmountGivenViewSet(viewsets.ModelViewSet):
    serializer_class = AmountGivenSerializer

    def perform_create(self, serializer):
        save_data = {}
        contact_pk = self.request.data.get('contact', None)
        if not contact_pk:
            raise ValidationError({'contact': ['Contact is required']})
        contact = Contact.objects.filter(
            user=self.request.user,
            pk=contact_pk
        ).first()
        if not contact:
            raise ValidationError({'contact': ['Contact does not exists']})
        save_data['contact'] = contact
        serializer.save(**save_data)

But when I add a new record to AmountGiven model and passing contact id in contact field

enter image description here

it is giving error as

{"contact":{"non_field_errors":["Invalid data. Expected a dictionary, but got str."]}}

When I remove contact = ContactSerializer() from AmountGivenSerializer, it works fine as expected but then in response as depth is set to 1, the contact data contains only model fields and not other property fields defined.

Answer

slider picture slider · Oct 19, 2018

I'm not a big fan of this request parsing pattern. From what I understand, you want to be able to see all the contact's details when you retrieve an AmountGiven object and at the same time be able to create and update AmountGiven by just providing the contact id.

So you can change your AmountGiven serializer to have 2 fields for the contact model field. Like this:

class AmountGivenSerializer(serializers.ModelSerializer):
    contact_detail = ContactSerializer(source='contact', read_only=True)

    class Meta:
        model = AmountGiven
        depth = 1
        fields = (
            'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
        )

Note that the contact_detail field has a source attribute.

Now the default functionality for create and update should work out of the box (validation and everything).

And when you retrieve an AmountGiven object, you should get all the details for the contact in the contact_detail field.

Update

I missed that you need to check whether the Contact belongs to a user (however, I don't see a user field on your Contact model, maybe you missed posting it). You can simplify that check:

class AmountGivenViewSet(viewsets.ModelViewSet):
    serializer_class = AmountGivenSerializer

    def perform_create(self, serializer):
        contact = serializer.validated_data.get('contact')
        if contact.user != self.request.user:
            raise ValidationError({'contact': ['Not a valid contact']})
        serializer.save()