Skip to content

fix(validators): guard UniqueConstraint validators against queryset instance in ListSerializer (fixes #9484)#10000

Open
AdarshJ173 wants to merge 1 commit into
encode:mainfrom
AdarshJ173:fix/9484-list-serializer-unique-constraint-queryset-pk-crash
Open

fix(validators): guard UniqueConstraint validators against queryset instance in ListSerializer (fixes #9484)#10000
AdarshJ173 wants to merge 1 commit into
encode:mainfrom
AdarshJ173:fix/9484-list-serializer-unique-constraint-queryset-pk-crash

Conversation

@AdarshJ173

Copy link
Copy Markdown

Problem

Fixes #9484AttributeError: 'BaseQuerySet' object has no attribute 'pk' when using ModelSerializer with many=True on a model that has a UniqueConstraint.

Root Cause

When many=True is passed to a ModelSerializer, many_init creates a ListSerializer wrapping the child serializer. The instance kwarg (a QuerySet) is forwarded to both the ListSerializer and the child serializer. When the child's UniqueTogetherValidator (or UniqueValidator) runs during validation, it calls:

def exclude_current_instance(self, attrs, queryset, instance):
    if instance is not None:
        return queryset.exclude(pk=instance.pk)  # 💥 QuerySet has no .pk
    return queryset

Because instance is the entire QuerySet, not a model instance, .pk raises AttributeError.

Reproducer (from #9484)

class Pet(models.Model):
    name = models.CharField(max_length=100)
    animal_type = models.CharField(max_length=100)
    can_fly = models.BooleanField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint(fields=["name", "animal_type"], name="unique_pet")
        ]

class PetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pet
        fields = ('name', 'animal_type', 'can_fly')

class PetListSerializer(serializers.ListSerializer):
    child = PetSerializer()

    def create(self, validated_data):
        return Pet.objects.bulk_create([Pet(**item) for item in validated_data])

new_data = [
    {"name": "Polly", "animal_type": "Parrot", "can_fly": True},
    {"name": "Penguin", "animal_type": "Bird", "can_fly": False},
]
serializer = PetListSerializer(Pet.objects.all(), data=new_data, many=True)
serializer.is_valid(raise_exception=True)  # AttributeError: 'BaseQuerySet' object has no attribute 'pk'

Fix

Introduces a small private helper _is_single_instance(instance) that returns True only if instance is a non-None, non-QuerySet, non-list object that has a .pk attribute. Both UniqueValidator.exclude_current_instance and UniqueTogetherValidator.exclude_current_instance (and BaseUniqueForValidator.exclude_current_instance) now use this helper instead of if instance is not None.

This is minimal and fully backward-compatible:

  • Single-instance update: unchanged — instance.pk still used to exclude the current object.
  • Bulk create (ListSerializer + queryset instance): validators no longer crash; treat each child item as a create.
  • None instance (plain create): unchanged.

The fix also corrects UniqueTogetherValidator.filter_queryset and __call__ which had the same latent bug of reading from serializer.instance without guarding against a queryset type.

Checklist

  • Minimal, targeted fix (one helper, three call sites updated)
  • Backward compatible — no API changes
  • Fixes crash in UniqueValidator, UniqueTogetherValidator, and BaseUniqueForValidator
  • Covers the filter_queryset path in UniqueTogetherValidator (latent bug: getattr(serializer.instance, source) would also crash if instance is a queryset)

…nstance

Fixes encode#9484

When a ModelSerializer is used with many=True and an instance queryset
(e.g. for bulk create via ListSerializer), the child serializer receives
the parent queryset as its `.instance` instead of a model instance.

This causes a hard crash in `UniqueTogetherValidator.exclude_current_instance`
and `UniqueValidator.exclude_current_instance` when they call
`queryset.exclude(pk=instance.pk)` — because a QuerySet has no `.pk` attribute:

    AttributeError: 'BaseQuerySet' object has no attribute 'pk'

Reproducing case (from issue encode#9484):

    class PetSerializer(serializers.ModelSerializer):
        class Meta:
            model = Pet  # has UniqueConstraint on (name, animal_type)
            fields = ('name', 'animal_type', 'can_fly')

    # This crashes on .is_valid()
    serializer = PetListSerializer(
        instances,  # <- queryset, not a single instance
        data=[{"name": "Polly", "animal_type": "Parrot", "can_fly": True}, ...],
        many=True,
    )

Root cause:
    `many_init` correctly passes `instance` to the ListSerializer, but
    the child serializer (PetSerializer) inherits the same `instance`
    reference — which is the queryset. When the UniqueConstraint validators
    run on the child, `serializer.instance` is the queryset, and `.pk`
    is undefined on it.

Fix:
    Add a `_is_single_instance` helper that returns True only when
    `instance` is a non-None, non-queryset, non-list object that has
    a `.pk` attribute. Both `UniqueValidator.exclude_current_instance` and
    `UniqueTogetherValidator.exclude_current_instance` now call this
    helper before accessing `instance.pk`, treating a queryset/list as
    "no instance" (i.e. a create operation) for the purposes of the
    uniqueness exclusion.

This is the minimal, backward-compatible fix:
  - Single-instance update: behaviour unchanged (instance.pk still used)
  - Bulk create (ListSerializer + queryset): validators no longer crash,
    correctly treat each item as a create
  - Existing tests: all pass unchanged

@auvipy auvipy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will also need appropriate test coverage to verify

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

List Serializer Breaks on Unique Constraint Validator

2 participants