Fix validators crashing with AttributeError when many=True passes a queryset as instance#9975
Conversation
…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
…ializer many=True
|
|
||
| self.enforce_required_fields(attrs, serializer) | ||
| queryset = self.queryset | ||
| queryset = self.filter_queryset(attrs, queryset, serializer) |
There was a problem hiding this comment.
| queryset = self.filter_queryset(attrs, queryset, serializer) | |
| queryset = self.filter_queryset(attrs, queryset, instance) |
| # 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 |
There was a problem hiding this comment.
| instance = serializer.instance if isinstance(serializer.instance, Model) else None |
There was a problem hiding this comment.
| def filter_queryset(self, attrs, queryset, instance): |
| }, | ||
| ] | ||
| serializer = UniqueConstraintSerializer(instances, data=data, many=True) | ||
| # Before the fix this raised: AttributeError: 'QuerySet' object has no attribute 'pk' |
There was a problem hiding this comment.
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.
|
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. |
|
@LeSingh1 I'm in favor of this PR. Lets wait for other people . meanwhile you should fix the pre-commit ci |
There was a problem hiding this comment.
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.pkwheninstanceis aModel. - Adjusted
UniqueTogetherValidatorto avoid reading attributes from non-Modelinstances during uniqueness checks. - Added a regression test ensuring
many=True+ querysetinstanceno longer raisesAttributeError.
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.
| } | ||
| if missing_items: | ||
| raise ValidationError(missing_items, code='required') | ||
|
|
||
| def filter_queryset(self, attrs, queryset, serializer): | ||
| def filter_queryset(self, attrs, queryset, instance): |
There was a problem hiding this comment.
@LeSingh1 please cross check this and other open suggestions
| 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, |
| instance = serializer.instance if isinstance(serializer.instance, Model) else None | ||
| self._sources = [ | ||
| serializer.fields[field_name].source | ||
| for field_name in self.fields | ||
| ] |
| @@ -513,7 +513,7 @@ def filter(self, **kwargs): | |||
| serializer = UniquenessTogetherSerializer(instance=self.instance) | |||
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
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. |
When a serializer is used with
many=Trueand 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, andBaseUniqueForValidator-- all callinstance.pkorgetattr(instance, field)insideexclude_current_instanceand related methods without checking that the instance is actually a Model object. This causes: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=Trueshould overriderun_child_validationto setself.child.instanceto the individual object, as the existing docstring already documents. This change just stops the crash for the case whererun_child_validationis not overridden.Closes #9484
Before the fix:
After the fix
is_valid()runs without error when the submitted data does not conflict with any existing uniqueness constraints.