Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/api-guide/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,41 @@ class MyView(APIView):
...
```

### OperationId

The schema generator generates an [operationid](openapi-operationid) for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "ListItems", "RetrieveItem", "UpdateItem", etc..

If you have several views with the same model, the generator may generate duplicate operationId.
In order to work around this, you can override the second part of the operationId: operation name.

```python
from rest_framework.schemas.openapi import AutoSchema

class ExampleView(APIView):
"""APIView subclass with custom schema introspection."""
schema = AutoSchema(operation_id_base="Custom")
```

The previous example will generate the following operationId: "ListCustoms", "RetrieveCustom", "UpdateCustom", "PartialUpdateCustom", "DestroyCustom".
You need to provide the singular form of he operation name. For the list operation, a "s" will be appended at the end of the operation.

If you need more configuration over the `operationId` field, you can override the `get_operation_id_base` and `get_operation_id` methods from the `AutoSchema` class:

```python
class CustomSchema(AutoSchema):
def get_operation_id_base(self, path, method, action):
pass

def get_operation_id(self, path, method):
pass

class CustomView(APIView):
"""APIView subclass with custom schema introspection."""
schema = CustomSchema()
```

[openapi]: https://github.com/OAI/OpenAPI-Specification
[openapi-specification-extensions]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions
[openapi-operation]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
[openapi-tags]: https://swagger.io/specification/#tagObject
[openapi-operationid]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#fixed-fields-17
42 changes: 29 additions & 13 deletions rest_framework/schemas/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@ def get_schema(self, request=None, public=False):

class AutoSchema(ViewInspector):

def __init__(self, tags=None):
def __init__(self, operation_id_base=None, tags=None):
"""
:param operation_id_base: user-defined name in operationId. If empty, it will be deducted from the Model/Serializer/View name.
"""
if tags and not all(isinstance(tag, str) for tag in tags):
raise ValueError('tags must be a list or tuple of string.')
self._tags = tags
self.operation_id_base = operation_id_base
super().__init__()

request_media_types = []
Expand All @@ -91,7 +95,7 @@ def __init__(self, tags=None):
def get_operation(self, path, method):
operation = {}

operation['operationId'] = self._get_operation_id(path, method)
operation['operationId'] = self.get_operation_id(path, method)
operation['description'] = self.get_description(path, method)

parameters = []
Expand All @@ -108,21 +112,17 @@ def get_operation(self, path, method):

return operation

def _get_operation_id(self, path, method):
def get_operation_id_base(self, path, method, action):
"""
Compute an operation ID from the model, serializer or view name.
Compute the base part for operation ID from the model, serializer or view name.
"""
method_name = getattr(self.view, 'action', method.lower())
if is_list_view(path, method, self.view):
action = 'list'
elif method_name not in self.method_mapping:
action = method_name
else:
action = self.method_mapping[method.lower()]
model = getattr(getattr(self.view, 'queryset', None), 'model', None)

if self.operation_id_base is not None:
name = self.operation_id_base

# Try to deduce the ID from the view's model
model = getattr(getattr(self.view, 'queryset', None), 'model', None)
if model is not None:
elif model is not None:
name = model.__name__

# Try with the serializer class name
Expand All @@ -147,6 +147,22 @@ def _get_operation_id(self, path, method):
if action == 'list' and not name.endswith('s'): # listThings instead of listThing
name += 's'

return name

def get_operation_id(self, path, method):
"""
Compute an operation ID from the view type and get_operation_id_base method.
"""
method_name = getattr(self.view, 'action', method.lower())
if is_list_view(path, method, self.view):
action = 'list'
elif method_name not in self.method_mapping:
action = method_name
else:
action = self.method_mapping[method.lower()]

name = self.get_operation_id_base(path, method, action)

return action + name

def _get_path_parameters(self, path, method):
Expand Down
68 changes: 67 additions & 1 deletion tests/schemas/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,75 @@ def test_operation_id_generation(self):
inspector = AutoSchema()
inspector.view = view

operationId = inspector._get_operation_id(path, method)
operationId = inspector.get_operation_id(path, method)
assert operationId == 'listExamples'

def test_operation_id_custom_operation_id_base(self):
path = '/'
method = 'GET'

view = create_view(
views.ExampleGenericAPIView,
method,
create_request(path),
)
inspector = AutoSchema(operation_id_base="Ulysse")
inspector.view = view

operationId = inspector.get_operation_id(path, method)
assert operationId == 'listUlysses'

def test_operation_id_custom_name(self):
path = '/'
method = 'GET'

view = create_view(
views.ExampleGenericAPIView,
method,
create_request(path),
)
inspector = AutoSchema(operation_id_base='Ulysse')
inspector.view = view

operationId = inspector.get_operation_id(path, method)
assert operationId == 'listUlysses'

def test_operation_id_override_get(self):
class CustomSchema(AutoSchema):
def get_operation_id(self, path, method):
return 'myCustomOperationId'

path = '/'
method = 'GET'
view = create_view(
views.ExampleGenericAPIView,
method,
create_request(path),
)
inspector = CustomSchema()
inspector.view = view

operationId = inspector.get_operation_id(path, method)
assert operationId == 'myCustomOperationId'

def test_operation_id_override_base(self):
class CustomSchema(AutoSchema):
def get_operation_id_base(self, path, method, action):
return 'Item'

path = '/'
method = 'GET'
view = create_view(
views.ExampleGenericAPIView,
method,
create_request(path),
)
inspector = CustomSchema()
inspector.view = view

operationId = inspector.get_operation_id(path, method)
assert operationId == 'listItem'

def test_repeat_operation_ids(self):
router = routers.SimpleRouter()
router.register('account', views.ExampleGenericViewSet, basename="account")
Expand Down