'Why is Django serializer converting object to string? ('Invalid data. Expected a dictionary, but got str.')

I'm using django-treebeard to bulk create (load_bulk) a bunch of items nested into a list in this format:

items = [
    {
        "data": {"item_id": i1.id},
    },
    {
        "data": {"item_id": i2.id},
    },
    {
        "data": {"item_id": i3.id},
    },
]
payload = {"list_id": l1.id, "items": items}

Here is my models.py:

class List(MP_Node):
    item = models.ForeignKey(Item, null=True, blank=True)

My views.py:

class ListParentBulkCreateView(generics.CreateAPIView):
    queryset = List.objects.all()
    serializer_class = ListBulkCreateSerializer
    permission_classes = [AllowAny]

My serializers.py:

class ListBulkCreateSerializer__data(serializers.ModelSerializer):
    item_id = serializers.UUIDField(required=True)

    class Meta:
        model = List
        fields = ["item_id"]

class ListBulkCreateSerializer_itemsWrapper(serializers.ModelSerializer):
    data = ListBulkCreateSerializer__data()
    
    class Meta:
        model = List
        fields = ["data"]

class ListBulkCreateSerializer(serializers.ModelSerializer):
    list_id = serializers.UUIDField(required=True)
    items = serializers.ListField(
        child=ListBulkCreateSerializer_itemsWrapper(), write_only=True
    )
    
    class Meta:
        model = List
        fields = ["list_id", "items"]

    def create(self, validated_data):
        # My code fails on is_valid() so
        # this doesn't even run
        list_id = validated_data["list_id"]
        list = List.objects.get(id=list_id)
        items = validated_data("items")
        # create tree
        List.load_bulk(items, parent=list)
        return {"list_id": list_id}
    

But I'm getting a 400 error when I try to create via a serializer. The serializer.error is:

{'parents': {0: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}, 1: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}, 2: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}}}

When I breakdown the CreateAPIView's create method, it reveals that the serializer is converting my list of objects into a list of strings:

class ListParentBulkCreateView(generics.CreateAPIView):
    ...

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        print(serializer.is_valid())
        print(serializer.errors)
        data = serializer.data
        print("0", data, type(data))
        print("1", data["parents"], type(data["parents"]))
        print("2", data["parents"][0], type(data["parents"][0]))
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(
            serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )

Returns:

False
{'parents': {0: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}, 1: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}, 2: {'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code='invalid')]}}}
0 <class 'rest_framework.utils.serializer_helpers.ReturnDict'>
1 <class 'list'>
2 <class 'str'>

Why is this happening? I can confirm my initial payload is correct:

print(payload["items"][0] # <class 'dict'>

Edit:

So after a bunch of trial and error, I got my code working if I json.dump my initial payload then json.load the request.data in my CreateAPIView create method....

But why on earth is this necessary?


items = [
    {
        "data": {"item_id": str(i1.id)},
    },
    {
        "data": {"item_id": str(i2.id)},
    },
    {
        "data": {"item_id": str(i3.id)},
    },
]
payload = {"list_id": str(l1.id), "items": items}

views.py

class ListParentBulkCreateView(generics.CreateAPIView):
    ...

    def create(self, request, *args, **kwargs):
        data = json.loads(request.data)
        serializer = self.get_serializer(data=data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(
            serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source