Add distinction between request and response serializers for OpenAPI (#7424)

* Add distinction between request and response serializers

* Add docs

* document new functions in schemas.md

* add a test case for different request vs response objects

* Correct formatting for flake8

Co-authored-by: Shaun Gosse <shaun.gosse@emburse.com>
This commit is contained in:
Denis Orehovsky 2021-04-20 17:03:16 +03:00 committed by GitHub
parent 010c8d4f08
commit 8812394ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 128 additions and 8 deletions

View File

@ -375,6 +375,20 @@ operationIds.
In order to work around this, you can override `get_operation_id_base()` to
provide a different base for name part of the ID.
#### `get_serializer()`
If the view has implemented `get_serializer()`, returns the result.
#### `get_request_serializer()`
By default returns `get_serializer()` but can be overridden to
differentiate between request and response objects.
#### `get_response_serializer()`
By default returns `get_serializer()` but can be overridden to
differentiate between request and response objects.
### `AutoSchema.__init__()` kwargs
`AutoSchema` provides a number of `__init__()` kwargs that can be used for

View File

@ -192,15 +192,22 @@ class AutoSchema(ViewInspector):
if method.lower() == 'delete':
return {}
serializer = self.get_serializer(path, method)
request_serializer = self.get_request_serializer(path, method)
response_serializer = self.get_response_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
return {}
components = {}
component_name = self.get_component_name(serializer)
if isinstance(request_serializer, serializers.Serializer):
component_name = self.get_component_name(request_serializer)
content = self.map_serializer(request_serializer)
components.setdefault(component_name, content)
content = self.map_serializer(serializer)
return {component_name: content}
if isinstance(response_serializer, serializers.Serializer):
component_name = self.get_component_name(response_serializer)
content = self.map_serializer(response_serializer)
components.setdefault(component_name, content)
return components
def _to_camel_case(self, snake_str):
components = snake_str.split('_')
@ -615,6 +622,20 @@ class AutoSchema(ViewInspector):
.format(view.__class__.__name__, method, path))
return None
def get_request_serializer(self, path, method):
"""
Override this method if your view uses a different serializer for
handling request body.
"""
return self.get_serializer(path, method)
def get_response_serializer(self, path, method):
"""
Override this method if your view uses a different serializer for
populating response data.
"""
return self.get_serializer(path, method)
def _get_reference(self, serializer):
return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
@ -624,7 +645,7 @@ class AutoSchema(ViewInspector):
self.request_media_types = self.map_parsers(path, method)
serializer = self.get_serializer(path, method)
serializer = self.get_request_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
item_schema = {}
@ -648,7 +669,7 @@ class AutoSchema(ViewInspector):
self.response_media_types = self.map_renderers(path, method)
serializer = self.get_serializer(path, method)
serializer = self.get_response_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
item_schema = {}

View File

@ -712,6 +712,91 @@ class TestOperationIntrospection(TestCase):
operationId = inspector.get_operation_id(path, method)
assert operationId == 'listItem'
def test_different_request_response_objects(self):
class RequestSerializer(serializers.Serializer):
text = serializers.CharField()
class ResponseSerializer(serializers.Serializer):
text = serializers.BooleanField()
class CustomSchema(AutoSchema):
def get_request_serializer(self, path, method):
return RequestSerializer()
def get_response_serializer(self, path, method):
return ResponseSerializer()
path = '/'
method = 'POST'
view = create_view(
views.ExampleGenericAPIView,
method,
create_request(path),
)
inspector = CustomSchema()
inspector.view = view
components = inspector.get_components(path, method)
assert components == {
'Request': {
'properties': {
'text': {
'type': 'string'
}
},
'required': ['text'],
'type': 'object'
},
'Response': {
'properties': {
'text': {
'type': 'boolean'
}
},
'required': ['text'],
'type': 'object'
}
}
operation = inspector.get_operation(path, method)
assert operation == {
'operationId': 'createExample',
'description': '',
'parameters': [],
'requestBody': {
'content': {
'application/json': {
'schema': {
'$ref': '#/components/schemas/Request'
}
},
'application/x-www-form-urlencoded': {
'schema': {
'$ref': '#/components/schemas/Request'
}
},
'multipart/form-data': {
'schema': {
'$ref': '#/components/schemas/Request'
}
}
}
},
'responses': {
'201': {
'content': {
'application/json': {
'schema': {
'$ref': '#/components/schemas/Response'
}
}
},
'description': ''
}
},
'tags': ['']
}
def test_repeat_operation_ids(self):
router = routers.SimpleRouter()
router.register('account', views.ExampleGenericViewSet, basename="account")