Skip to content

Commit bc4d525

Browse files
author
Dhaval Mehta
authored
Schemas: Add mapping of type for ChoiceField. (#7161)
1 parent 160f912 commit bc4d525

File tree

2 files changed

+55
-7
lines changed

2 files changed

+55
-7
lines changed

rest_framework/schemas/openapi.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import warnings
2+
from collections import OrderedDict
3+
from decimal import Decimal
24
from operator import attrgetter
35
from urllib.parse import urljoin
46

@@ -209,6 +211,34 @@ def _get_pagination_parameters(self, path, method):
209211

210212
return paginator.get_schema_operation_parameters(view)
211213

214+
def _map_choicefield(self, field):
215+
choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates
216+
if all(isinstance(choice, bool) for choice in choices):
217+
type = 'boolean'
218+
elif all(isinstance(choice, int) for choice in choices):
219+
type = 'integer'
220+
elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
221+
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
222+
type = 'number'
223+
elif all(isinstance(choice, str) for choice in choices):
224+
type = 'string'
225+
else:
226+
type = None
227+
228+
mapping = {
229+
# The value of `enum` keyword MUST be an array and SHOULD be unique.
230+
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20
231+
'enum': choices
232+
}
233+
234+
# If We figured out `type` then and only then we should set it. It must be a string.
235+
# Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type
236+
# It is optional but it can not be null.
237+
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
238+
if type:
239+
mapping['type'] = type
240+
return mapping
241+
212242
def _map_field(self, field):
213243

214244
# Nested Serializers, `many` or not.
@@ -242,15 +272,11 @@ def _map_field(self, field):
242272
if isinstance(field, serializers.MultipleChoiceField):
243273
return {
244274
'type': 'array',
245-
'items': {
246-
'enum': list(field.choices)
247-
},
275+
'items': self._map_choicefield(field)
248276
}
249277

250278
if isinstance(field, serializers.ChoiceField):
251-
return {
252-
'enum': list(field.choices),
253-
}
279+
return self._map_choicefield(field)
254280

255281
# ListField.
256282
if isinstance(field, serializers.ListField):

tests/schemas/test_openapi.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
import pytest
24
from django.conf.urls import url
35
from django.test import RequestFactory, TestCase, override_settings
@@ -44,6 +46,8 @@ def test_pagination(self):
4446

4547
class TestFieldMapping(TestCase):
4648
def test_list_field_mapping(self):
49+
uuid1 = uuid.uuid4()
50+
uuid2 = uuid.uuid4()
4751
inspector = AutoSchema()
4852
cases = [
4953
(serializers.ListField(), {'items': {}, 'type': 'array'}),
@@ -53,7 +57,25 @@ def test_list_field_mapping(self):
5357
(serializers.ListField(child=serializers.IntegerField(max_value=4294967295)),
5458
{'items': {'type': 'integer', 'maximum': 4294967295, 'format': 'int64'}, 'type': 'array'}),
5559
(serializers.ListField(child=serializers.ChoiceField(choices=[('a', 'Choice A'), ('b', 'Choice B')])),
56-
{'items': {'enum': ['a', 'b']}, 'type': 'array'}),
60+
{'items': {'enum': ['a', 'b'], 'type': 'string'}, 'type': 'array'}),
61+
(serializers.ListField(child=serializers.ChoiceField(choices=[(1, 'One'), (2, 'Two')])),
62+
{'items': {'enum': [1, 2], 'type': 'integer'}, 'type': 'array'}),
63+
(serializers.ListField(child=serializers.ChoiceField(choices=[(1.1, 'First'), (2.2, 'Second')])),
64+
{'items': {'enum': [1.1, 2.2], 'type': 'number'}, 'type': 'array'}),
65+
(serializers.ListField(child=serializers.ChoiceField(choices=[(True, 'true'), (False, 'false')])),
66+
{'items': {'enum': [True, False], 'type': 'boolean'}, 'type': 'array'}),
67+
(serializers.ListField(child=serializers.ChoiceField(choices=[(uuid1, 'uuid1'), (uuid2, 'uuid2')])),
68+
{'items': {'enum': [uuid1, uuid2]}, 'type': 'array'}),
69+
(serializers.ListField(child=serializers.ChoiceField(choices=[(1, 'One'), ('a', 'Choice A')])),
70+
{'items': {'enum': [1, 'a']}, 'type': 'array'}),
71+
(serializers.ListField(child=serializers.ChoiceField(choices=[
72+
(1, 'One'), ('a', 'Choice A'), (1.1, 'First'), (1.1, 'First'), (1, 'One'), ('a', 'Choice A'), (1, 'One')
73+
])),
74+
{'items': {'enum': [1, 'a', 1.1]}, 'type': 'array'}),
75+
(serializers.ListField(child=serializers.ChoiceField(choices=[
76+
(1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'),
77+
])),
78+
{'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}),
5779
(serializers.IntegerField(min_value=2147483648),
5880
{'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}),
5981
]

0 commit comments

Comments
 (0)