Skip to content

Commit d295b9f

Browse files
committed
Implement OpenAPI Components
1 parent 2a5c2f3 commit d295b9f

File tree

4 files changed

+368
-84
lines changed

4 files changed

+368
-84
lines changed

docs/api-guide/schemas.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ This also applies to extra actions for `ViewSet`s:
215215
If you wish to provide a base `AutoSchema` subclass to be used throughout your
216216
project you may adjust `settings.DEFAULT_SCHEMA_CLASS` appropriately.
217217

218+
<<<<<<< HEAD
218219

219220
### Grouping Operations With Tags
220221

@@ -288,8 +289,72 @@ class MyView(APIView):
288289
...
289290
```
290291

292+
=======
293+
### Components
294+
295+
Since DRF 3.12, Schema uses the [OpenAPI Components](openapi-components). This method define components in the schema and [referenced them](openapi-reference) inside request and response objects. The component's name is deduced from the Serializer's name.
296+
297+
Using OpenAPI's components provides the following advantages:
298+
* The schema is more readable and lightweight.
299+
* If you use the schema to generate a SDK (using [openapi-generator](openapi-generator) or [swagger-codegen](swagger-codegen)). The generator can name your SDK's models.
300+
301+
### Handling component's schema errors
302+
303+
You may get the following error while generating the schema:
304+
```
305+
"Serializer" is an invalid class name for schema generation.
306+
Serializer's class name should be unique and explicit. e.g. "ItemSerializer".
307+
```
308+
309+
This error occurs when the Serializer name is "Serializer". You should choose a component's name unique across your schema and different than "Serializer".
310+
311+
You may also get the following warning:
312+
```
313+
Schema component "ComponentName" has been overriden with a different value.
314+
```
315+
316+
This warning occurs when different components have the same name in one schema. Your component name should be unique across your project. This is likely an error that may lead to an invalid schema.
317+
318+
You have two ways to solve the previous issues:
319+
* You can rename your serializer with a unique name and another name than "Serializer".
320+
* You can set the `component_name` kwarg parameter of the AutoSchema constructor (see below).
321+
* You can override the `get_component_name` method of the AutoSchema class (see below).
322+
323+
#### Set a custom component's name for your view
324+
325+
To override the component's name in your view, you can use the `component_name` parameter of the AutoSchema constructor:
326+
327+
```python
328+
from rest_framework.schemas.openapi import AutoSchema
329+
330+
class MyView(APIView):
331+
schema = AutoSchema(component_name="Ulysses")
332+
```
333+
334+
#### Override the default implementation
335+
336+
If you want to have more control and customization about how the schema's components are generated, you can override the `get_component_name` and `get_components` method from the AutoSchema class.
337+
338+
```python
339+
from rest_framework.schemas.openapi import AutoSchema
340+
341+
class CustomSchema(AutoSchema):
342+
def get_components(self, path, method):
343+
# Implement your custom implementation
344+
345+
def get_component_name(self, serializer):
346+
# Implement your custom implementation
347+
348+
class CustomView(APIView):
349+
"""APIView subclass with custom schema introspection."""
350+
schema = CustomSchema()
351+
```
291352

292353
[openapi]: https://github.com/OAI/OpenAPI-Specification
293354
[openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions
294355
[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
295356
[openapi-tags]: https://swagger.io/specification/#tagObject
357+
[openapi-components]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#componentsObject
358+
[openapi-reference]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#referenceObject
359+
[openapi-generator]: https://github.com/OpenAPITools/openapi-generator
360+
[swagger-codegen]: https://github.com/swagger-api/swagger-codegen

rest_framework/schemas/openapi.py

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import warnings
23
from collections import OrderedDict
34
from decimal import Decimal
@@ -39,16 +40,26 @@ def get_schema(self, request=None, public=False):
3940
Generate a OpenAPI schema.
4041
"""
4142
self._initialise_endpoints()
43+
components_schemas = {}
4244

4345
# Iterate endpoints generating per method path operations.
44-
# TODO: …and reference components.
4546
paths = {}
4647
_, view_endpoints = self._get_paths_and_endpoints(None if public else request)
4748
for path, method, view in view_endpoints:
4849
if not self.has_view_permissions(path, method, view):
4950
continue
5051

5152
operation = view.schema.get_operation(path, method)
53+
components = view.schema.get_components(path, method)
54+
for k in components.keys():
55+
if k not in components_schemas:
56+
continue
57+
if components_schemas[k] == components[k]:
58+
continue
59+
warnings.warn('Schema component "{}" has been overriden with a different value.'.format(k))
60+
61+
components_schemas.update(components)
62+
5263
# Normalise path for any provided mount url.
5364
if path.startswith('/'):
5465
path = path[1:]
@@ -64,6 +75,11 @@ def get_schema(self, request=None, public=False):
6475
'paths': paths,
6576
}
6677

78+
if len(components_schemas) > 0:
79+
schema['components'] = {
80+
'schemas': components_schemas
81+
}
82+
6783
return schema
6884

6985
# View Inspectors
@@ -88,6 +104,13 @@ def __init__(self, tags=None):
88104
'delete': 'Destroy',
89105
}
90106

107+
def __init__(self, component_name=None):
108+
"""
109+
:param component_name: user-defined component's name. If empty, it will be deducted from the Serializer's class name.
110+
"""
111+
super().__init__()
112+
self.component_name = component_name
113+
91114
def get_operation(self, path, method):
92115
operation = {}
93116

@@ -108,6 +131,43 @@ def get_operation(self, path, method):
108131

109132
return operation
110133

134+
def get_component_name(self, serializer):
135+
"""
136+
Compute the component's name from the serializer.
137+
Raise an exception if the serializer's class name is "Serializer" (case-insensitive).
138+
"""
139+
if self.component_name is not None:
140+
return self.component_name
141+
142+
# use the serializer's class name as the component name.
143+
component_name = serializer.__class__.__name__
144+
# We remove the "serializer" string from the class name.
145+
pattern = re.compile("serializer", re.IGNORECASE)
146+
component_name = pattern.sub("", component_name)
147+
148+
if component_name == "":
149+
raise Exception(
150+
'"{}" is an invalid class name for schema generation. '
151+
'Serializer\'s class name should be unique and explicit. e.g. "ItemSerializer"'
152+
.format(serializer.__class__.__name__)
153+
)
154+
155+
return component_name
156+
157+
def get_components(self, path, method):
158+
"""
159+
Return components with their properties from the serializer.
160+
"""
161+
serializer = self._get_serializer(path, method)
162+
163+
if not isinstance(serializer, serializers.Serializer):
164+
return {}
165+
166+
component_name = self.get_component_name(serializer)
167+
168+
content = self._map_serializer(serializer)
169+
return {component_name: content}
170+
111171
def _get_operation_id(self, path, method):
112172
"""
113173
Compute an operation ID from the model, serializer or view name.
@@ -390,10 +450,6 @@ def _map_min_max(self, field, content):
390450

391451
def _map_serializer(self, serializer):
392452
# Assuming we have a valid serializer instance.
393-
# TODO:
394-
# - field is Nested or List serializer.
395-
# - Handle read_only/write_only for request/response differences.
396-
# - could do this with readOnly/writeOnly and then filter dict.
397453
required = []
398454
properties = {}
399455

@@ -498,6 +554,9 @@ def _get_serializer(self, path, method):
498554
.format(view.__class__.__name__, method, path))
499555
return None
500556

557+
def _get_reference(self, serializer):
558+
return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
559+
501560
def _get_request_body(self, path, method):
502561
if method not in ('PUT', 'PATCH', 'POST'):
503562
return {}
@@ -507,20 +566,13 @@ def _get_request_body(self, path, method):
507566
serializer = self._get_serializer(path, method)
508567

509568
if not isinstance(serializer, serializers.Serializer):
510-
return {}
511-
512-
content = self._map_serializer(serializer)
513-
# No required fields for PATCH
514-
if method == 'PATCH':
515-
content.pop('required', None)
516-
# No read_only fields for request.
517-
for name, schema in content['properties'].copy().items():
518-
if 'readOnly' in schema:
519-
del content['properties'][name]
569+
item_schema = {}
570+
else:
571+
item_schema = self._get_reference(serializer)
520572

521573
return {
522574
'content': {
523-
ct: {'schema': content}
575+
ct: {'schema': item_schema}
524576
for ct in self.request_media_types
525577
}
526578
}
@@ -536,17 +588,12 @@ def _get_responses(self, path, method):
536588

537589
self.response_media_types = self.map_renderers(path, method)
538590

539-
item_schema = {}
540591
serializer = self._get_serializer(path, method)
541592

542-
if isinstance(serializer, serializers.Serializer):
543-
item_schema = self._map_serializer(serializer)
544-
# No write_only fields for response.
545-
for name, schema in item_schema['properties'].copy().items():
546-
if 'writeOnly' in schema:
547-
del item_schema['properties'][name]
548-
if 'required' in item_schema:
549-
item_schema['required'] = [f for f in item_schema['required'] if f != name]
593+
if not isinstance(serializer, serializers.Serializer):
594+
item_schema = {}
595+
else:
596+
item_schema = self._get_reference(serializer)
550597

551598
if is_list_view(path, method, self.view):
552599
response_schema = {

0 commit comments

Comments
 (0)