Skip to content

Fix validators crashing with AttributeError when many=True passes a queryset as instance#9975

Open
LeSingh1 wants to merge 7 commits into
encode:mainfrom
LeSingh1:fix/unique-validators-many-true-queryset-instance
Open

Fix validators crashing with AttributeError when many=True passes a queryset as instance#9975
LeSingh1 wants to merge 7 commits into
encode:mainfrom
LeSingh1:fix/unique-validators-many-true-queryset-instance

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Jun 4, 2026

Copy link
Copy Markdown

When a serializer is used with many=True and a queryset as the instance argument, the child serializer's instance is set to the parent ListSerializer's queryset rather than a single model object. Three validators -- UniqueValidator, UniqueTogetherValidator, and BaseUniqueForValidator -- all call instance.pk or getattr(instance, field) inside exclude_current_instance and related methods without checking that the instance is actually a Model object. This causes:

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

The fix checks isinstance(instance, Model) at every place that reads from the instance. When the instance is a queryset rather than a model object the validators fall back to create semantics: no exclusion of a current object, and no field-value comparison against existing data.

Users who need proper per-object update semantics with many=True should override run_child_validation to set self.child.instance to the individual object, as the existing docstring already documents. This change just stops the crash for the case where run_child_validation is not overridden.

Closes #9484

Before the fix:

instances = Pet.objects.all()
serializer = PetSerializer(instances, data=[...], many=True)
serializer.is_valid()  # raises AttributeError: 'QuerySet' object has no attribute 'pk'

After the fix is_valid() runs without error when the submitted data does not conflict with any existing uniqueness constraints.

LeSingh1 added 3 commits June 4, 2026 16:12
…ueryset as instance

When a serializer is used with many=True and a queryset instance, the child
serializer's instance is set to the parent ListSerializer's queryset rather
than a single model object. UniqueValidator, UniqueTogetherValidator, and
BaseUniqueForValidator all called instance.pk or getattr(instance, field)
without checking that instance is actually a Model, which raises:

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

The fix guards every place that reads from instance by checking
isinstance(instance, Model) first. When the instance is a queryset the
validators fall back to create semantics (no exclusion, no field-value
comparison against the existing object). Users who need proper per-object
update semantics with many=True should override run_child_validation to
set self.child.instance to the individual object, as documented.

Fixes encode#9484
Comment thread rest_framework/validators.py Outdated

self.enforce_required_fields(attrs, serializer)
queryset = self.queryset
queryset = self.filter_queryset(attrs, queryset, serializer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
queryset = self.filter_queryset(attrs, queryset, serializer)
queryset = self.filter_queryset(attrs, queryset, instance)

Comment thread rest_framework/validators.py Outdated
# If this is an update, then any unprovided field should
# have it's value set based on the existing instance attribute.
if serializer.instance is not None:
instance = serializer.instance if isinstance(serializer.instance, Model) else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
instance = serializer.instance if isinstance(serializer.instance, Model) else None

Comment thread rest_framework/validators.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def filter_queryset(self, attrs, queryset, instance):

Comment thread tests/test_validators.py Outdated
},
]
serializer = UniqueConstraintSerializer(instances, data=data, many=True)
# Before the fix this raised: AttributeError: 'QuerySet' object has no attribute 'pk'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue: This should comment should be removed.

Sources are now resolved in __call__ where the serializer is available
and stored on self._sources so filter_queryset can use them when called
from __call__. Direct callers of filter_queryset fall back to self.fields.
The extraneous instance derivation inside filter_queryset and the now-stale
comment in the test are removed.
@LeSingh1

LeSingh1 commented Jun 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review Pravin. I've updated the branch with your suggestions. filter_queryset now takes instance directly instead of serializer, the internal derivation of instance inside that method is removed, and I updated the one test that was calling filter_queryset directly with a serializer= keyword. The sources list is now computed in call (where serializer is available) and stored temporarily on self so filter_queryset can use it; direct callers fall back to self.fields. The explanatory comment in the test is also gone. All 56 validator tests pass.

@p-r-a-v-i-n

p-r-a-v-i-n commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

@LeSingh1 I'm in favor of this PR. Lets wait for other people . meanwhile you should fix the pre-commit ci

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a crash in DRF uniqueness validators when many=True is used with a queryset passed as instance, by ensuring validators only treat instance as an updatable object when it is a Django Model instance (otherwise falling back to create semantics).

Changes:

  • Guarded exclude_current_instance() in multiple validators to only access .pk when instance is a Model.
  • Adjusted UniqueTogetherValidator to avoid reading attributes from non-Model instances during uniqueness checks.
  • Added a regression test ensuring many=True + queryset instance no longer raises AttributeError.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
rest_framework/validators.py Adds Model-type checks and updates uniqueness validator flow to avoid .pk/attribute access on querysets.
tests/test_validators.py Updates/extends validator tests and adds a regression case for many=True with queryset instance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 136 to +140
}
if missing_items:
raise ValidationError(missing_items, code='required')

def filter_queryset(self, attrs, queryset, serializer):
def filter_queryset(self, attrs, queryset, instance):

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.

@LeSingh1 please cross check this and other open suggestions

Comment on lines +140 to +144
def filter_queryset(self, attrs, queryset, instance):
"""
Filter the queryset to all instances matching the given attributes.
"""
# field names => field sources
sources = [
serializer.fields[field_name].source
for field_name in self.fields
]
# field names => field sources; resolved by __call__ when available,
Comment on lines +176 to +180
instance = serializer.instance if isinstance(serializer.instance, Model) else None
self._sources = [
serializer.fields[field_name].source
for field_name in self.fields
]
Comment thread tests/test_validators.py Outdated
@@ -513,7 +513,7 @@ def filter(self, **kwargs):
serializer = UniquenessTogetherSerializer(instance=self.instance)
Comment thread tests/test_validators.py
auvipy and others added 2 commits June 6, 2026 10:52
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@LeSingh1

LeSingh1 commented Jun 6, 2026

Copy link
Copy Markdown
Author

Fixed the pre-commit failures. There were two flake8 errors: an unused variable (F841) and a missing blank line before a class definition (E302), plus a codespell hit on 'normalised'. Also found that the partial-update test case was using non-conflicting global_id values (101, 202) so the UniqueValidator never fired -- switched those to 1 and 2 which already exist in the test DB. All 56 validator tests pass and pre-commit runs clean now.

@auvipy auvipy requested a review from browniebroke June 8, 2026 15:16
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

4 participants