Home > Blockchain >  Django rest framework valid related field not found by serializer but present in request
Django rest framework valid related field not found by serializer but present in request

Time:05-22

I have 2 related models and I am trying to perform an ajax 'multipart/form' post request. But it seems like data regarding the related model is not identified by the serializer for some reason. I have tried editing the 'create' method of the viewset, to understand why the data is not passed, but to no avail. I think the issue is related to json serialization, but i do not know how to fix it

Here are some things i tried:

models.py

class Dream(models.Model):
    # Some fields
    ...

class Milestone(models.Model):
    title = models.CharField(max_length=100)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    date = models.DateTimeField(auto_now_add=True)
    description = models.TextField(max_length=500)
    dream = models.ForeignKey(
        Dream, on_delete=models.CASCADE, related_name='milestones')

serializers.py

class DreamSerializer(TaggitSerializer, serializers.ModelSerializer):
    milestones = MilestoneSerializer(many=True, read_only=False)
    class Meta:
        model = Dream
        fields = (..., 'milestones')


class MilestoneSerializer(serializers.ModelSerializer):
    class Meta:
        model = Milestone
        fields = ('id', 'title', 'user', 'date', 'description', 'dream')
        read_only_fields = ('user', 'dream', 'date')

views.py

class DreamViewSet(viewsets.ModelViewSet):
    queryset = Dream.objects.prefetch_related('user').all()
    serializer_class = DreamSerializer
    permission_classes = [permissions.IsAuthenticated]

    # Tried to manually override the create function to fix the error
    def create(self, request, *args, **kwargs):
        print(request.data) # <QueryDict: {'title': ['test'], 'image':[<InMemoryUploadedFile: somePhoto.jpg (image/jpeg)>], ... , 'milestones': ['[{"title":"test1","description":"test1"}]']}>
        
        # seems like it is evaluating to string literal '[{"title":"test1","description":"test1"}]'
        print(request.data['milestones']) # [{"title":"test1","description":"test1"}]
        print(type(request.data['milestones'])) # <class 'str'>
        cleaned_data = request.data.copy()
        milestones = cleaned_data.pop('milestones', [])
        print(milestones) # ['[{"title":"test1","description":"test1"}]'] (type list)
        if len(milestones):
            # tried to manually deserialize data 
            cleaned_data['milestones'] = json.loads(milestones[0])

        milestone_serializer = MilestoneSerializer(
            data=cleaned_data['milestones'], many=True)
        print(cleaned_data) # <QueryDict: {'title': ['test'], 'image': [<InMemoryUploadedFile: somePhoto.jpg (image/jpeg)>], 'milestones': [[{'title': 'test1', 'description': 'test1'}]]}>
        print(milestone_serializer.is_valid()) # True
        print(milestone_serializer.data) # [OrderedDict([('title', 'test1'), ('description', 'test1')])]
        serializer = self.get_serializer(data=cleaned_data)

        if serializer.is_valid(): # False
            # not executedd
            print(serializer.validated_data)
            self.perform_create(serializer)
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=201, headers=headers)
        else:
            print(serializer.errors) # {'milestones': [ErrorDetail(string='This field is required.', code='required')]}

on the frontend:

async add({title, image, ..., milestones}) {
    const imageFile = new File([image], `somePhoto.jpg`, {
      type: image.type,
    });
    
    const formData = new FormData();
    formData.append("title", title);
    formData.append("image", imageFile);
    ....
    formData.append("milestones", JSON.stringify(milestones));

    await axios.post(endpoint, formData); // status: 400, statusText: 'Bad Request',  milestones: ['This field is required.']
}

CodePudding user response:

It doesn't work because in the DreamSerializer, you set the milestones field as the list of milestone objects. But in frontend you set it as string. You need to change that first. And I think create logic should be written in the create method of the serializer. First you define other field for writing data of the milestones.

import json

class DreamSerializer(TaggitSerializer, serializers.ModelSerializer):
    milestones = MilestoneSerializer(many=True, read_only=True)
    milestone_str = serializers.CharField(write_only = True)

    class Meta:
        model = Dream
        fields = (..., 'milestones', 'milestone_str')

    def create(self, validated_data):
        milestone_str = validated_data.pop('milestone_str')
        milestone_data = json.loads(milestone_str)

        milestone_serializer = MilestoneSerializer(
            data=milestone_data, many=True)

        if milestone_serializer.is_valid():

            # create Dream object first
            dream = Dream.objects.create(**validated_data)

            for milestone_item in milestone_data:
                Milestone.objects.create(dream = dream, **milestone_item)
            return dream
        else:
            raise serializers.ValidationError("invalid milestone data")

Then in views.py, you don't need to define create method.

class DreamViewSet(viewsets.ModelViewSet):
    queryset = Dream.objects.prefetch_related('user').all()
    serializer_class = DreamSerializer
    permission_classes = [permissions.IsAuthenticated]

In frontend,

async add({title, image, ..., milestones}) {
    ...

    formData.append("milestone_str", JSON.stringify(milestones));

    ...
}

Hope it could help.

  • Related