Django DRF, Cannot reproduce POST request using APIClient and APITestCase, payload fields are removed

I have the following setup in Django DRF:

class TagSerializer(serializers.Serializer):

    id = serializers.UUIDField(read_only=True)
    name = serializers.CharField(required=True)
    style = serializers.JSONField(read_only=True)


class TagCreationMixin:

    def create(self, validated_data):
        tags = validated_data.pop("tags", [])
        failure_mode = self.Meta.model.objects.create(**validated_data)
        for tag in tags:
            current_tag, _ = models.Tag.objects.get_or_create(**tag)
            failure_mode.tags.add(current_tag)
        return failure_mode

class ItemSerializer(TagCreationMixin, serializers.ModelSerializer):

    tags = TagSerializer(many=True, required=False)

    class Meta:
        model = models.Item
        fields = "__all__"

And I want to automatize tests using APIClient and APITestCase. I have the following test defined:

class TestAPIMissingTags(APITestCase):

    fixtures = [
        "core/fixtures/users.yaml"
    ]

    payload = {
        "name": "test",
        "tags": [{"name": "test"}, {"name": "critical"}],
    }

    def setUp(self):
        self.user = models.CustomUser.objects.get(username="jlandercy")
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_complete_payload_is_sent(self):
        response = self.client.post("/api/core/item/", data=self.payload)
        print(response)
        print(response.json())

Which returns a 201 but without any tags, it seems they are popped from the initial payload:

<Response status_code=201, "application/json">
{'id': 'f3cd6bbb-239f-4f47-9b2d-f648ead76bdd', 'tags': [], 'name': 'test'}

Anyway when I challenge Swagger with the same endoint and payload it works as expected:

{
  "id": "6d05c2e3-fce3-420d-bef9-4bccd28062f7",
  "tags": [
    {
      "id": "fa70c875-818b-4ca2-9417-4209fd377453",
      "name": "test",
      "style": {
        "background-color": "#cc7a13"
      }
    },
    {
      "id": "9fd59657-4242-432f-b165-1ed0a67546e3",
      "name": "critical",
      "style": {
        "background-color": "#bf8100"
      }
    }
  ],
  "name": "test"
}

Why cannot I reproduce the same behavior than Swagger using the APIClient.

I finally got some time to test a solution. I think we can fix it by using .to_internal_value() instead:

class TagSerializer(serializers.Serializer):
    id = serializers.UUIDField(read_only=True)
    name = serializers.CharField(required=True)
    style = serializers.JSONField(read_only=True)

    def to_internal_value(self, validated_data):
        data = dict(validated_data)
        name = data.pop('name')
        tag, __ = Tag.objects.get_or_create(name=name, defaults=data)
        return tag


class TagCreationMixin:

    def create(self, validated_data):
        data = dict(validated_data)
        tags = data.pop('tags', [])
        item = self.Meta.model.objects.create(**data)
        book.tags.add(*tags)
        return item


class ItemSerializer(TagCreationMixin, serializers.ModelSerializer):
    tags = TagSerializer(many=True, required=False)

    class Meta:
        model = models.Item
        fields = '__all__'

It is probably also better to work with shallow copies of the validated data, since Django's REST framework does not make a copy of the data itself, which can lead to unexpected behavior.

Вернуться на верх