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.