Skip to content

Commit b45ff07

Browse files
reupencarltongibson
authored andcommitted
Use an array type for list view response schemas
This is the first part of encode#6846. Previously, the response schema for list views was an object representing a single item. However, list views return a list of items, and hence it should be an array. Further work will need to be done to support how pagination classes modify list responses. There should be no change for views not determined to be list views.
1 parent a3f244d commit b45ff07

File tree

2 files changed

+138
-40
lines changed

2 files changed

+138
-40
lines changed

rest_framework/schemas/openapi.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -460,22 +460,30 @@ def _get_request_body(self, path, method):
460460
}
461461

462462
def _get_responses(self, path, method):
463-
# TODO: Handle multiple codes.
464-
content = {}
463+
# TODO: Handle multiple codes and pagination classes.
464+
item_schema = {}
465465
serializer = self._get_serializer(path, method)
466466

467467
if isinstance(serializer, serializers.Serializer):
468-
content = self._map_serializer(serializer)
468+
item_schema = self._map_serializer(serializer)
469469
# No write_only fields for response.
470-
for name, schema in content['properties'].copy().items():
470+
for name, schema in item_schema['properties'].copy().items():
471471
if 'writeOnly' in schema:
472-
del content['properties'][name]
473-
content['required'] = [f for f in content['required'] if f != name]
472+
del item_schema['properties'][name]
473+
item_schema['required'] = [f for f in item_schema['required'] if f != name]
474+
475+
if is_list_view(path, method, self.view):
476+
response_schema = {
477+
'type': 'array',
478+
'items': item_schema,
479+
}
480+
else:
481+
response_schema = item_schema
474482

475483
return {
476484
'200': {
477485
'content': {
478-
ct: {'schema': content}
486+
ct: {'schema': response_schema}
479487
for ct in self.content_types
480488
}
481489
}

tests/schemas/test_openapi.py

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,18 @@ def test_path_without_parameters(self):
8282
assert operation == {
8383
'operationId': 'ListExamples',
8484
'parameters': [],
85-
'responses': {'200': {'content': {'application/json': {'schema': {}}}}},
85+
'responses': {
86+
'200': {
87+
'content': {
88+
'application/json': {
89+
'schema': {
90+
'type': 'array',
91+
'items': {},
92+
},
93+
},
94+
},
95+
},
96+
},
8697
}
8798

8899
def test_path_with_id_parameter(self):
@@ -184,6 +195,83 @@ class View(generics.GenericAPIView):
184195
assert list(schema['properties']['nested']['properties'].keys()) == ['number']
185196
assert schema['properties']['nested']['required'] == ['number']
186197

198+
def test_list_response_body_generation(self):
199+
"""Test that an array schema is returned for list views."""
200+
path = '/'
201+
method = 'GET'
202+
203+
class ItemSerializer(serializers.Serializer):
204+
text = serializers.CharField()
205+
206+
class View(generics.GenericAPIView):
207+
serializer_class = ItemSerializer
208+
209+
view = create_view(
210+
View,
211+
method,
212+
create_request(path),
213+
)
214+
inspector = AutoSchema()
215+
inspector.view = view
216+
217+
responses = inspector._get_responses(path, method)
218+
assert responses == {
219+
'200': {
220+
'content': {
221+
'application/json': {
222+
'schema': {
223+
'type': 'array',
224+
'items': {
225+
'properties': {
226+
'text': {
227+
'type': 'string',
228+
},
229+
},
230+
'required': ['text'],
231+
},
232+
},
233+
},
234+
},
235+
},
236+
}
237+
238+
def test_retrieve_response_body_generation(self):
239+
"""Test that a list of properties is returned for retrieve item views."""
240+
path = '/{id}/'
241+
method = 'GET'
242+
243+
class ItemSerializer(serializers.Serializer):
244+
text = serializers.CharField()
245+
246+
class View(generics.GenericAPIView):
247+
serializer_class = ItemSerializer
248+
249+
view = create_view(
250+
View,
251+
method,
252+
create_request(path),
253+
)
254+
inspector = AutoSchema()
255+
inspector.view = view
256+
257+
responses = inspector._get_responses(path, method)
258+
assert responses == {
259+
'200': {
260+
'content': {
261+
'application/json': {
262+
'schema': {
263+
'properties': {
264+
'text': {
265+
'type': 'string',
266+
},
267+
},
268+
'required': ['text'],
269+
},
270+
},
271+
},
272+
},
273+
}
274+
187275
def test_operation_id_generation(self):
188276
path = '/'
189277
method = 'GET'
@@ -226,10 +314,11 @@ def test_serializer_datefield(self):
226314
inspector.view = view
227315

228316
responses = inspector._get_responses(path, method)
229-
response_schema = responses['200']['content']['application/json']['schema']['properties']
230-
assert response_schema['date']['type'] == response_schema['datetime']['type'] == 'string'
231-
assert response_schema['date']['format'] == 'date'
232-
assert response_schema['datetime']['format'] == 'date-time'
317+
response_schema = responses['200']['content']['application/json']['schema']
318+
properties = response_schema['items']['properties']
319+
assert properties['date']['type'] == properties['datetime']['type'] == 'string'
320+
assert properties['date']['format'] == 'date'
321+
assert properties['datetime']['format'] == 'date-time'
233322

234323
def test_serializer_validators(self):
235324
path = '/'
@@ -243,45 +332,46 @@ def test_serializer_validators(self):
243332
inspector.view = view
244333

245334
responses = inspector._get_responses(path, method)
246-
response_schema = responses['200']['content']['application/json']['schema']['properties']
335+
response_schema = responses['200']['content']['application/json']['schema']
336+
properties = response_schema['items']['properties']
247337

248-
assert response_schema['integer']['type'] == 'integer'
249-
assert response_schema['integer']['maximum'] == 99
250-
assert response_schema['integer']['minimum'] == -11
338+
assert properties['integer']['type'] == 'integer'
339+
assert properties['integer']['maximum'] == 99
340+
assert properties['integer']['minimum'] == -11
251341

252-
assert response_schema['string']['minLength'] == 2
253-
assert response_schema['string']['maxLength'] == 10
342+
assert properties['string']['minLength'] == 2
343+
assert properties['string']['maxLength'] == 10
254344

255-
assert response_schema['regex']['pattern'] == r'[ABC]12{3}'
256-
assert response_schema['regex']['description'] == 'must have an A, B, or C followed by 1222'
345+
assert properties['regex']['pattern'] == r'[ABC]12{3}'
346+
assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222'
257347

258-
assert response_schema['decimal1']['type'] == 'number'
259-
assert response_schema['decimal1']['multipleOf'] == .01
260-
assert response_schema['decimal1']['maximum'] == 10000
261-
assert response_schema['decimal1']['minimum'] == -10000
348+
assert properties['decimal1']['type'] == 'number'
349+
assert properties['decimal1']['multipleOf'] == .01
350+
assert properties['decimal1']['maximum'] == 10000
351+
assert properties['decimal1']['minimum'] == -10000
262352

263-
assert response_schema['decimal2']['type'] == 'number'
264-
assert response_schema['decimal2']['multipleOf'] == .0001
353+
assert properties['decimal2']['type'] == 'number'
354+
assert properties['decimal2']['multipleOf'] == .0001
265355

266-
assert response_schema['email']['type'] == 'string'
267-
assert response_schema['email']['format'] == 'email'
268-
assert response_schema['email']['default'] == 'foo@bar.com'
356+
assert properties['email']['type'] == 'string'
357+
assert properties['email']['format'] == 'email'
358+
assert properties['email']['default'] == 'foo@bar.com'
269359

270-
assert response_schema['url']['type'] == 'string'
271-
assert response_schema['url']['nullable'] is True
272-
assert response_schema['url']['default'] == 'http://www.example.com'
360+
assert properties['url']['type'] == 'string'
361+
assert properties['url']['nullable'] is True
362+
assert properties['url']['default'] == 'http://www.example.com'
273363

274-
assert response_schema['uuid']['type'] == 'string'
275-
assert response_schema['uuid']['format'] == 'uuid'
364+
assert properties['uuid']['type'] == 'string'
365+
assert properties['uuid']['format'] == 'uuid'
276366

277-
assert response_schema['ip4']['type'] == 'string'
278-
assert response_schema['ip4']['format'] == 'ipv4'
367+
assert properties['ip4']['type'] == 'string'
368+
assert properties['ip4']['format'] == 'ipv4'
279369

280-
assert response_schema['ip6']['type'] == 'string'
281-
assert response_schema['ip6']['format'] == 'ipv6'
370+
assert properties['ip6']['type'] == 'string'
371+
assert properties['ip6']['format'] == 'ipv6'
282372

283-
assert response_schema['ip']['type'] == 'string'
284-
assert 'format' not in response_schema['ip']
373+
assert properties['ip']['type'] == 'string'
374+
assert 'format' not in properties['ip']
285375

286376

287377
@pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.')

0 commit comments

Comments
 (0)