Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f0375ca
Fix #8926: ListSerializer preserves instance for many=True during val…
zainnadeem786 Jan 25, 2026
ac82e50
Update rest_framework/serializers.py
auvipy Feb 24, 2026
07de4b8
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Feb 24, 2026
c402a57
Fix #8926 with minimal ListSerializer instance matching changes
zainnadeem786 Feb 24, 2026
66b8012
Keep virtualenv ignored in .gitignore
zainnadeem786 Feb 24, 2026
90e1a24
Fix Copilot/auvipy review: safe iterable check, restore save() assert…
zainnadeem786 Feb 24, 2026
0acf49a
Refine ListSerializer review follow-ups and cleanup
zainnadeem786 Feb 24, 2026
1484520
Restore serializers docstrings from upstream main
zainnadeem786 Feb 25, 2026
ef7e976
Update rest_framework/serializers.py
auvipy Feb 25, 2026
c665595
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Feb 25, 2026
b61b472
Remove unreachable return in ListSerializer.to_internal_value
zainnadeem786 Feb 25, 2026
22caa96
Update rest_framework/serializers.py
auvipy Feb 25, 2026
de40cb5
Merge branch 'main' into improve-many-true-validation-guidance
browniebroke Feb 26, 2026
5176e44
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Mar 2, 2026
c9665dd
Address review follow-ups in ListSerializer internals
zainnadeem786 Mar 2, 2026
83e9965
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Mar 14, 2026
21417c8
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Mar 17, 2026
781cf7e
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Mar 27, 2026
5bdf57e
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Mar 31, 2026
f08921e
Fix ListSerializer run_child_validation and save() issues
zainnadeem786 Mar 31, 2026
bb6b3bb
Merge branch 'improve-many-true-validation-guidance' of https://githu…
zainnadeem786 Mar 31, 2026
dcb4ad1
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Apr 5, 2026
2fa19ed
Refine ListSerializer instance matching: reduce scope, add fallback b…
zainnadeem786 Apr 7, 2026
dd07e02
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Apr 13, 2026
2a84879
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Apr 30, 2026
7ce5b8f
Potential fix for pull request finding
auvipy May 3, 2026
e0129a3
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Jun 9, 2026
341665c
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Jun 9, 2026
be54fe2
Address ListSerializer instance map review feedback
zainnadeem786 Jun 9, 2026
71ad671
Merge branch 'main' into improve-many-true-validation-guidance
auvipy Jun 10, 2026
c5f6024
Remove unrelated not_a_list error change
zainnadeem786 Jun 10, 2026
8156fd6
Address ListSerializer lookup field review feedback
zainnadeem786 Jun 11, 2026
9a4fffb
Fix ListSerializer supports instance access during validation for man…
zainnadeem786 Jun 11, 2026
3469b44
Merge branch 'main' into improve-many-true-validation-guidance
zainnadeem786 Sep 7, 2026
52f9bf0
Fix serializer validation merge conflicts
zainnadeem786 Sep 7, 2026
f7ff10c
Preserve unsupported ListSerializer instance behavior
zainnadeem786 Sep 7, 2026
2489cab
Address ListSerializer review feedback
zainnadeem786 Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/env/
MANIFEST
coverage.*
venv/
.coverage
.cache/

Expand Down
11 changes: 6 additions & 5 deletions docs/api-guide/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,8 @@ To support multiple updates you'll need to do so explicitly. When writing your m

You will need to add an explicit `id` field to the instance serializer. The default implicitly-generated `id` field is marked as `read_only`. This causes it to be removed on updates. Once you declare it explicitly, it will be available in the list serializer's `update` method.

During validation, `ListSerializer` matches each input item to an existing instance using `id` or `pk`. To use another identifier, such as `uuid`, set `lookup_field` on the child serializer's `Meta` class.

Here's an example of how you might choose to implement multiple updates:

class BookListSerializer(serializers.ListSerializer):
Expand Down Expand Up @@ -855,14 +857,13 @@ Here's an example of how you might choose to implement multiple updates:

class Meta:
list_serializer_class = BookListSerializer
lookup_field = 'id'
Comment thread
zainnadeem786 marked this conversation as resolved.

If the child serializer includes uniqueness validators (`UniqueValidator`, `UniqueTogetherValidator`, or the
`UniqueForDateValidator` family), they need to know which object each item in the list is updating, so
that the object itself is not reported as a uniqueness conflict. By default the child serializer's
`.instance` is the whole queryset or list that was passed to the list serializer, so these validators will
raise a `RuntimeError` during a multiple update. To support this, override `run_child_validation()` on
your `ListSerializer` subclass to set the child's `.instance` and `.initial_data` for each item before
validation. For example, if `self.instance` is a queryset:
that the object itself is not reported as a uniqueness conflict. `ListSerializer` sets the child's
`.instance` and `.initial_data` for each matched item before validation. For custom matching behavior,
override `run_child_validation()` on your `ListSerializer` subclass. For example, if `self.instance` is a queryset:

class BookListSerializer(serializers.ListSerializer):
def run_child_validation(self, data):
Expand Down
135 changes: 113 additions & 22 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,46 @@ def run_child_validation(self, data):
self.child.initial_data = data
return super().run_child_validation(data)
"""
return self.child.run_validation(data)
if not hasattr(self.child, 'instance'):
return self.child.run_validation(data)

if not (
hasattr(self, '_list_serializer_instance_map') and
isinstance(data, Mapping)
):
return self.child.run_validation(data)

lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', None)
original_instance = self.child.instance
if original_instance is not self.instance:
return self.child.run_validation(data)

if lookup_field is not None:
data_pk = data.get(lookup_field)
else:
data_pk = data.get('id')
if data_pk is None:
data_pk = data.get('pk')

child_instance = (
self._list_serializer_instance_map.get(str(data_pk))
if data_pk is not None else None
)

has_initial_data = hasattr(self.child, 'initial_data')
if has_initial_data:
original_initial_data = self.child.initial_data

try:
self.child.instance = child_instance
self.child.initial_data = data
return self.child.run_validation(data)
Comment thread
zainnadeem786 marked this conversation as resolved.
finally:
self.child.instance = original_instance
Comment thread
zainnadeem786 marked this conversation as resolved.
if has_initial_data:
self.child.initial_data = original_initial_data
elif hasattr(self.child, 'initial_data'):
delattr(self.child, 'initial_data')

def to_internal_value(self, data):
"""
Expand Down Expand Up @@ -702,28 +741,69 @@ def to_internal_value(self, data):
ret = []
errors = {}

for index, item in enumerate(data):
try:
validated = self.run_child_validation(item)
except ValidationError as exc:
errors[index] = exc.detail
# Build a primary key lookup for instance matching in many=True updates.
instance_map = None
if self.instance is not None:
if isinstance(self.instance, Mapping):
instance_map = {str(k): v for k, v in self.instance.items()}
Comment thread
zainnadeem786 marked this conversation as resolved.
else:
ret.append(validated)
instance_iterable = self.instance
if isinstance(instance_iterable, models.manager.BaseManager):
instance_iterable = instance_iterable.all()
if not isinstance(instance_iterable, (list, tuple, models.query.QuerySet)):
instance_iterable = None

if instance_iterable is not None:
instance_map = {}
lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', None)

for obj in instance_iterable:
if lookup_field is not None:
lookup_values = [getattr(obj, lookup_field, None)]
else:
lookup_values = [
getattr(obj, 'id', None),
getattr(obj, 'pk', None),
]

for lookup_value in lookup_values:
if lookup_value is not None:
instance_map[str(lookup_value)] = obj

has_instance_map = hasattr(self, '_list_serializer_instance_map')
if has_instance_map:
original_instance_map = self._list_serializer_instance_map
if instance_map is not None:
self._list_serializer_instance_map = instance_map

if errors:
if not api_settings.LIST_SERIALIZER_ERRORS_AS_DICT:
warnings.warn(
'The list-based error format for `ListSerializer` is '
'deprecated and will be removed in DRF 3.20. Set '
'`REST_FRAMEWORK["LIST_SERIALIZER_ERRORS_AS_DICT"]` to '
'`True` to use the dictionary-based error format.',
RemovedInDRF320Warning,
stacklevel=4,
)
errors = [errors.get(index, {}) for index in range(len(data))]
raise ValidationError(errors)
try:
for index, item in enumerate(data):
try:
validated = self.run_child_validation(item)
except ValidationError as exc:
errors[index] = exc.detail
else:
ret.append(validated)

if errors:
if not api_settings.LIST_SERIALIZER_ERRORS_AS_DICT:
warnings.warn(
'The list-based error format for `ListSerializer` is '
'deprecated and will be removed in DRF 3.20. Set '
'`REST_FRAMEWORK["LIST_SERIALIZER_ERRORS_AS_DICT"]` to '
'`True` to use the dictionary-based error format.',
RemovedInDRF320Warning,
stacklevel=4,
)
errors = [errors.get(index, {}) for index in range(len(data))]
raise ValidationError(errors)

return ret
return ret
finally:
if instance_map is not None and has_instance_map:
self._list_serializer_instance_map = original_instance_map
elif instance_map is not None and hasattr(self, '_list_serializer_instance_map'):
delattr(self, '_list_serializer_instance_map')

def to_representation(self, data):
"""
Expand Down Expand Up @@ -758,16 +838,27 @@ def save(self, **kwargs):
"""
Save and return a list of object instances.
"""
assert hasattr(self, '_errors'), (
'You must call `.is_valid()` before calling `.save()`.'
)
assert not self.errors, (
'You cannot call `.save()` on a serializer with invalid data.'
)
Comment thread
zainnadeem786 marked this conversation as resolved.

# Guard against incorrect use of `serializer.save(commit=False)`
assert 'commit' not in kwargs, (
"'commit' is not a valid keyword argument to the 'save()' method. "
"If you need to access data before committing to the database then "
"inspect 'serializer.validated_data' instead. "
"You can also pass additional keyword arguments to 'save()' if you "
"need to set extra attributes on the saved model instance. "
"For example: 'serializer.save(owner=request.user)'.'"
"For example: 'serializer.save(owner=request.user)'."
)
assert not hasattr(self, '_data'), (
"You cannot call `.save()` after accessing `serializer.data`. "
"If you need to access data before committing to the database then "
"inspect 'serializer.validated_data' instead. "
)
Comment thread
zainnadeem786 marked this conversation as resolved.

validated_data = [
{**attrs, **kwargs} for attrs in self.validated_data
]
Expand Down
Loading