From 792bc4d5527ab32665c97ba9a4b2a5f351ecbecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20Vargov=C4=8D=C3=ADk?= Date: Wed, 27 Apr 2022 22:57:51 +0200 Subject: [PATCH] openapi: remove JSON body second validation and type casting (#1170) * openapi: remove body preprocessing Body is already validated using jsonschema. There was also some type casting but it was wrong: e.g. not recurring deeply into dicts and lists, relying on existence of "type" in schema (which is not there e.g. if oneOf is used). Anyway, the only reason why types should be casted is converting integer values to float if the type is number. But this is in most cases irrelevant. Added an example, which did not work before this commit (echoed `{}`) e.g. for ``` curl localhost:8080/api/foo -H 'content-type: application/json' -d '{"foo": 1}' ``` but now the example works (echoes `{"foo": 1}`). * test with oneOf in the requestBody * remove oneof examples: superseded by tests Co-authored-by: Pavol Vargovcik --- connexion/operations/openapi.py | 38 ++++++++++++++++-------------- tests/api/test_responses.py | 31 ++++++++++++++++++++++++ tests/fixtures/simple/openapi.yaml | 17 +++++++++++++ 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/connexion/operations/openapi.py b/connexion/operations/openapi.py index 7f5d0de3a..176eb9669 100644 --- a/connexion/operations/openapi.py +++ b/connexion/operations/openapi.py @@ -9,6 +9,7 @@ from connexion.operations.abstract import AbstractOperation from ..decorators.uri_parsing import OpenAPIURIParser +from ..http_facts import FORM_CONTENT_TYPES from ..utils import deep_get, deep_merge, is_null, is_nullable, make_type logger = logging.getLogger("connexion.operations.openapi3") @@ -286,13 +287,28 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize): 'the requestBody instead.', DeprecationWarning) x_body_name = sanitize(self.body_schema.get('x-body-name', 'body')) + if self.consumes[0] in FORM_CONTENT_TYPES: + result = self._get_body_argument_form(body) + else: + result = self._get_body_argument_json(body) + + if x_body_name in arguments or has_kwargs: + return {x_body_name: result} + return {} + + def _get_body_argument_json(self, body): # if the body came in null, and the schema says it can be null, we decide # to include no value for the body argument, rather than the default body if is_nullable(self.body_schema) and is_null(body): - if x_body_name in arguments or has_kwargs: - return {x_body_name: None} - return {} + return None + + if body is None: + default_body = self.body_schema.get('default', {}) + return deepcopy(default_body) + return body + + def _get_body_argument_form(self, body): # now determine the actual value for the body (whether it came in or is default) default_body = self.body_schema.get('default', {}) body_props = {k: {"schema": v} for k, v @@ -302,25 +318,11 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize): # see: https://github.com/OAI/OpenAPI-Specification/blame/3.0.2/versions/3.0.2.md#L2305 additional_props = self.body_schema.get("additionalProperties", True) - if body is None: - body = deepcopy(default_body) - - # if the body isn't even an object, then none of the concerns below matter - if self.body_schema.get("type") != "object": - if x_body_name in arguments or has_kwargs: - return {x_body_name: body} - return {} - - # supply the initial defaults and convert all values to the proper types by schema body_arg = deepcopy(default_body) body_arg.update(body or {}) - res = {} if body_props or additional_props: - res = self._get_typed_body_values(body_arg, body_props, additional_props) - - if x_body_name in arguments or has_kwargs: - return {x_body_name: res} + return self._get_typed_body_values(body_arg, body_props, additional_props) return {} def _get_typed_body_values(self, body_arg, body_props, additional_props): diff --git a/tests/api/test_responses.py b/tests/api/test_responses.py index e5a6f38be..da76bbfba 100644 --- a/tests/api/test_responses.py +++ b/tests/api/test_responses.py @@ -388,3 +388,34 @@ def test_streaming_response(simple_app): app_client = simple_app.app.test_client() resp = app_client.get('/v1.0/get_streaming_response') assert resp.status_code == 200 + + +def test_oneof(simple_openapi_app): + app_client = simple_openapi_app.app.test_client() + + post_greeting = app_client.post( # type: flask.Response + '/v1.0/oneof_greeting', + data=json.dumps({"name": 3}), + content_type="application/json" + ) + assert post_greeting.status_code == 200 + assert post_greeting.content_type == 'application/json' + greeting_reponse = json.loads(post_greeting.data.decode('utf-8', 'replace')) + assert greeting_reponse['greeting'] == 'Hello 3' + + post_greeting = app_client.post( # type: flask.Response + '/v1.0/oneof_greeting', + data=json.dumps({"name": True}), + content_type="application/json" + ) + assert post_greeting.status_code == 200 + assert post_greeting.content_type == 'application/json' + greeting_reponse = json.loads(post_greeting.data.decode('utf-8', 'replace')) + assert greeting_reponse['greeting'] == 'Hello True' + + post_greeting = app_client.post( # type: flask.Response + '/v1.0/oneof_greeting', + data=json.dumps({"name": "jsantos"}), + content_type="application/json" + ) + assert post_greeting.status_code == 400 diff --git a/tests/fixtures/simple/openapi.yaml b/tests/fixtures/simple/openapi.yaml index 749abcb3f..9ab6f6915 100644 --- a/tests/fixtures/simple/openapi.yaml +++ b/tests/fixtures/simple/openapi.yaml @@ -1222,6 +1222,23 @@ paths: schema: type: string format: binary + /oneof_greeting: + post: + operationId: fakeapi.hello.post_greeting3 + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + oneOf: + - {type: boolean} + - {type: number} + additionalProperties: false + responses: + '200': + description: Echo the validated request. servers: - url: http://localhost:{port}/{basePath}