From b537c687677d558e7944b08f54e12d146001e99b Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 20 Jun 2026 12:25:47 -0700 Subject: [PATCH 1/3] Fix TypeError in RelatedField.get_choices() when to_representation returns unhashable type When a subclass overrides to_representation() to return an unhashable type such as a dict, RelatedField.get_choices() raises: TypeError: unhashable type: 'dict' because the return value is used directly as a dictionary key. Fix by converting the representation to str() before using it as the key. This is safe since HTML select option values are always strings. Fixes #5141 --- rest_framework/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 4409bce77c..0f3c0cd037 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -199,7 +199,7 @@ def get_choices(self, cutoff=None): queryset = queryset[:cutoff] return { - self.to_representation(item): self.display_value(item) for item in queryset + str(self.to_representation(item)): self.display_value(item) for item in queryset } @property From 072cfa2e3754b42019061561249957185ffed810 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Thu, 9 Jul 2026 10:08:08 -0700 Subject: [PATCH 2/3] fix: use try/except in get_choices to preserve hashable key types; add tests --- rest_framework/relations.py | 11 ++++++++--- tests/test_relations.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 0f3c0cd037..47452cd6f4 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -198,9 +198,14 @@ def get_choices(self, cutoff=None): if cutoff is not None: queryset = queryset[:cutoff] - return { - str(self.to_representation(item)): self.display_value(item) for item in queryset - } + result = {} + for item in queryset: + value = self.to_representation(item) + try: + result[value] = self.display_value(item) + except TypeError: + result[str(value)] = self.display_value(item) + return result @property def choices(self): diff --git a/tests/test_relations.py b/tests/test_relations.py index b9ab157896..7a6d555ec6 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -509,6 +509,41 @@ def test_get_value_multi_dictionary_partial(self): assert empty == self.field.get_value(mvd) +class TestRelatedFieldGetChoicesUnhashable(APISimpleTestCase): + def test_get_choices_with_unhashable_to_representation(self): + """get_choices() should not raise TypeError when to_representation returns an unhashable type.""" + queryset = MockQueryset([ + MockObject(pk=1, name='foo'), + MockObject(pk=2, name='bar'), + ]) + + class DictRelatedField(serializers.RelatedField): + def to_representation(self, obj): + return {'id': obj.pk, 'name': obj.name} + + def to_internal_value(self, data): + pass + + field = DictRelatedField(read_only=True) + field.queryset = queryset + choices = field.get_choices() + assert choices == { + "{'id': 1, 'name': 'foo'}": 'MockObject name=foo, pk=1', + "{'id': 2, 'name': 'bar'}": 'MockObject name=bar, pk=2', + } + + def test_get_choices_with_hashable_to_representation_unchanged(self): + """get_choices() should return original type keys when to_representation returns a hashable.""" + queryset = MockQueryset([ + MockObject(pk=1, name='foo'), + MockObject(pk=2, name='bar'), + ]) + field = serializers.PrimaryKeyRelatedField(queryset=queryset) + choices = field.get_choices() + # Keys should remain ints (not stringified), preserving existing behavior + assert list(choices.keys()) == [1, 2] + + class TestHyperlink: def setup_method(self): self.default_hyperlink = serializers.Hyperlink('http://example.com', 'test') From 4ac2a10b38cab37828bed12e7a0097f0750f1051 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Fri, 10 Jul 2026 07:35:34 -0700 Subject: [PATCH 3/3] fix: correct expected display_value in get_choices unhashable test MockObject.__str__ wraps its representation in angle brackets, so display_value() returns ''. Align the expected choices mapping with that output so the test passes. --- tests/test_relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index 7a6d555ec6..b72b0266b5 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -528,8 +528,8 @@ def to_internal_value(self, data): field.queryset = queryset choices = field.get_choices() assert choices == { - "{'id': 1, 'name': 'foo'}": 'MockObject name=foo, pk=1', - "{'id': 2, 'name': 'bar'}": 'MockObject name=bar, pk=2', + "{'id': 1, 'name': 'foo'}": '', + "{'id': 2, 'name': 'bar'}": '', } def test_get_choices_with_hashable_to_representation_unchanged(self):