Django DRF, create an Item with associated tags whether they exist or not

I have the following create function for ItemSerializer class. It aims to create new Item with tags, creating tags on the fly if they does not exist or getting them if they exist.

class TagSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.Tag
        fields = ("id", "name", "style")

class ItemSerializer(serializers.ModelSerializer):

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

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

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

Anyway when I perform a POST on the Item with already existing tag:

 {
    "tags": [{"name": "My Tag"}],
    "name": "My Item"
 }

I get the following DRF 400 answer:

{
  "tags": [
    {
      "name": [
        "tag with this name already exists."
      ]
    }
  ]
}

It seems in this case my create function is skipped and Django DRF tries to create the tag anyway.

The main reason this happens is because when you create something with a serializer, and there is a sub-serializer, it will also try to create items for the sub-serialzer(s) first.

Since the name of the Tag is unique, it will thus not start creating any tags, because the constraint fails, and since it can not create (all) of the specified Tag, so does the Item.

Perhaps a bit of a "un-elegant" solution might be to make a serializer that does not enforces that the name is unique, and then creates or retrieves the corresponding Tag, like:

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

    def create(self, validated_data):
        name = validated_data.pop(name)
        tag, __ = Tag.objects.get_or_create(
            name=validated_data['name'], defaults=validated_data
        )
        return tag


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

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

    # no create

Thank you to Willem, it helped a lot to point out how to solve this problem.

Changing the TagSerializer for Serializer instead of ModelSerializer was sufficient to make the mechanics work:

class TagSerializer(serializers.Serializer):

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

class ItemSerializer(serializers.ModelSerializer):

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

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

    def create(self, validated_data):
        tags = validated_data.pop("tags", [])
        failure_mode = models.Item.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

It can then be refactored into a Mixin to reuse with another entities having tags:

class TagCreationMixin:

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

It must be the first inline in the MRO in order to make it works:

class ItemSerializer(TagCreationMixin, serializers.ModelSerializer):
    ...

If not, it raises the following error:

AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `core.serializers.ItemSerializer`, or set `read_only=True` on nested serializer fields.
[18/May/2025 09:04:01] "POST /api/core/item/ HTTP/1.1" 500 7765

Anyway it does not solve the issue to reproduce tag creation with Unit Tests.

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