Skip to content

Commit

Permalink
openapi: remove JSON body second validation and type casting (spec-fi…
Browse files Browse the repository at this point in the history
…rst#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 <pavol.vargovcik@kiwi.com>
  • Loading branch information
2 people authored and Gaetano Guerriero committed Aug 11, 2022
1 parent af8b978 commit dc10644
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 18 deletions.
38 changes: 20 additions & 18 deletions especifico/operations/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from especifico.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("especifico.operations.openapi3")
Expand Down Expand Up @@ -307,13 +308,28 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
)
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 in self.body_schema.get("properties", {}).items()}
Expand All @@ -322,25 +338,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):
Expand Down
31 changes: 31 additions & 0 deletions tests/api/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,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
17 changes: 17 additions & 0 deletions tests/fixtures/simple/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit dc10644

Please sign in to comment.