Skip to content

Commit 8156fd6

Browse files
committed
Address ListSerializer lookup field review feedback
1 parent c5f6024 commit 8156fd6

3 files changed

Lines changed: 23 additions & 84 deletions

File tree

docs/api-guide/serializers.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,8 @@ To support multiple updates you'll need to do so explicitly. When writing your m
799799

800800
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.
801801

802+
During validation, `ListSerializer` matches each input item to an existing instance using `pk` by default. To use another identifier, such as `id` or `uuid`, set `lookup_field` on the child serializer's `Meta` class.
803+
802804
Here's an example of how you might choose to implement multiple updates:
803805

804806
class BookListSerializer(serializers.ListSerializer):
@@ -831,6 +833,7 @@ Here's an example of how you might choose to implement multiple updates:
831833

832834
class Meta:
833835
list_serializer_class = BookListSerializer
836+
lookup_field = 'id'
834837

835838
### Customizing ListSerializer initialization
836839

rest_framework/serializers.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -673,13 +673,8 @@ def run_child_validation(self, data):
673673
):
674674
return self.child.run_validation(data)
675675

676-
lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', None)
677-
if lookup_field is not None:
678-
data_pk = data.get(lookup_field)
679-
else:
680-
data_pk = data.get('id')
681-
if data_pk is None:
682-
data_pk = data.get('pk')
676+
lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', 'pk')
677+
data_pk = data.get(lookup_field)
683678

684679
if data_pk is None:
685680
return self.child.run_validation(data)
@@ -738,15 +733,10 @@ def to_internal_value(self, data):
738733
instance_map = {str(k): v for k, v in self.instance.items()}
739734
elif isinstance(self.instance, (list, tuple, models.query.QuerySet)):
740735
instance_map = {}
741-
lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', None)
736+
lookup_field = getattr(getattr(self.child, 'Meta', None), 'lookup_field', 'pk')
742737

743738
for obj in self.instance:
744-
if lookup_field is not None:
745-
pk = getattr(obj, lookup_field, None)
746-
else:
747-
pk = getattr(obj, 'id', None)
748-
if pk is None:
749-
pk = getattr(obj, 'pk', None)
739+
pk = getattr(obj, lookup_field, None)
750740

751741
if pk is not None:
752742
key = str(pk)
@@ -831,7 +821,7 @@ def save(self, **kwargs):
831821
"For example: 'serializer.save(owner=request.user)'."
832822
)
833823
assert not hasattr(self, '_data'), (
834-
"You cannot call `.save()` after accessing `serializer.data`."
824+
"You cannot call `.save()` after accessing `serializer.data`. "
835825
"If you need to access data before committing to the database then "
836826
"inspect 'serializer.validated_data' instead. "
837827
)

tests/test_serializer_lists.py

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -204,30 +204,7 @@ def update(self, instance, validated_data):
204204

205205

206206
class TestListSerializerInstanceMatching:
207-
def test_matching_with_id(self):
208-
seen_instances = []
209-
210-
class TestSerializer(serializers.Serializer):
211-
id = serializers.IntegerField()
212-
213-
def validate(self, attrs):
214-
seen_instances.append(self.instance)
215-
return attrs
216-
217-
instance = [
218-
BasicObject(id=1),
219-
BasicObject(id=2),
220-
]
221-
input_data = [
222-
{'id': 1},
223-
{'id': 2},
224-
]
225-
226-
serializer = TestSerializer(instance, data=input_data, many=True)
227-
assert serializer.is_valid()
228-
assert seen_instances == instance
229-
230-
def test_matching_with_pk(self):
207+
def test_matching_with_default_lookup_field(self):
231208
seen_instances = []
232209

233210
class TestSerializer(serializers.Serializer):
@@ -250,59 +227,25 @@ def validate(self, attrs):
250227
assert serializer.is_valid()
251228
assert seen_instances == instance
252229

253-
def test_matching_with_id_against_object_with_pk_only(self):
254-
seen_instances = []
255-
256-
class TestSerializer(serializers.Serializer):
257-
id = serializers.IntegerField()
258-
259-
def validate(self, attrs):
260-
seen_instances.append(self.instance)
261-
return attrs
262-
263-
instance = [BasicObject(pk=1)]
264-
input_data = [{'id': 1}]
265-
266-
serializer = TestSerializer(instance, data=input_data, many=True)
267-
assert serializer.is_valid()
268-
assert seen_instances == instance
269-
270-
def test_matching_prefers_id_when_id_and_pk_differ(self):
271-
seen_instances = []
272-
273-
class TestSerializer(serializers.Serializer):
274-
id = serializers.IntegerField()
275-
276-
def validate(self, attrs):
277-
seen_instances.append(self.instance)
278-
return attrs
279-
280-
instance = [BasicObject(id=1, pk=2)]
281-
input_data = [{'id': 1}]
282-
283-
serializer = TestSerializer(instance, data=input_data, many=True)
284-
assert serializer.is_valid()
285-
assert seen_instances == instance
286-
287230
def test_mapping_instance_matching(self):
288231
seen_instances = []
289232

290233
class TestSerializer(serializers.Serializer):
291-
id = serializers.IntegerField()
234+
pk = serializers.IntegerField()
292235

293236
def validate(self, attrs):
294237
seen_instances.append(self.instance)
295238
return attrs
296239

297-
obj1 = BasicObject(id=1)
298-
obj2 = BasicObject(id=2)
240+
obj1 = BasicObject(pk=1)
241+
obj2 = BasicObject(pk=2)
299242
instance = {
300243
'1': obj1,
301244
'2': obj2,
302245
}
303246
input_data = [
304-
{'id': 1},
305-
{'id': 2},
247+
{'pk': 1},
248+
{'pk': 2},
306249
]
307250

308251
serializer = TestSerializer(instance, data=input_data, many=True)
@@ -313,13 +256,13 @@ def test_unsupported_instance_type_preserves_original_behavior(self):
313256
seen_instances = []
314257

315258
class TestSerializer(serializers.Serializer):
316-
id = serializers.IntegerField()
259+
pk = serializers.IntegerField()
317260

318261
def validate(self, attrs):
319262
seen_instances.append(self.instance)
320263
return attrs
321264

322-
serializer = TestSerializer(instance=123, data=[{'id': 1}], many=True)
265+
serializer = TestSerializer(instance=123, data=[{'pk': 1}], many=True)
323266
assert serializer.is_valid()
324267
assert seen_instances == [123]
325268

@@ -370,11 +313,11 @@ def validate(self, attrs):
370313

371314
def test_existing_instance_map_is_restored_after_validation(self):
372315
class TestSerializer(serializers.Serializer):
373-
id = serializers.IntegerField()
316+
pk = serializers.IntegerField()
374317

375-
instance = [BasicObject(id=1)]
376-
original_instance_map = {'sentinel': BasicObject(id=2)}
377-
serializer = TestSerializer(instance, data=[{'id': 1}], many=True)
318+
instance = [BasicObject(pk=1)]
319+
original_instance_map = {'sentinel': BasicObject(pk=2)}
320+
serializer = TestSerializer(instance, data=[{'pk': 1}], many=True)
378321
serializer._list_serializer_instance_map = original_instance_map
379322

380323
assert serializer.is_valid()
@@ -1073,6 +1016,9 @@ class TestSerializer(serializers.Serializer):
10731016
id = serializers.IntegerField()
10741017
status = serializers.CharField()
10751018

1019+
class Meta:
1020+
lookup_field = 'id'
1021+
10761022
def validate_status(self, value):
10771023
if self.instance is None:
10781024
raise serializers.ValidationError("Instance not matched")

0 commit comments

Comments
 (0)