Skip to content

Commit 11510f0

Browse files
committed
Raw value type strict validation
1 parent 395f68b commit 11510f0

File tree

6 files changed

+240
-39
lines changed

6 files changed

+240
-39
lines changed

openapi_core/schema/parameters/models.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ def unmarshal(self, value, custom_formatters=None):
109109
raise InvalidParameterValue(self.name, exc)
110110

111111
try:
112-
unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters)
112+
unmarshalled = self.schema.unmarshal(
113+
deserialized,
114+
custom_formatters=custom_formatters,
115+
strict=False,
116+
)
113117
except OpenAPISchemaError as exc:
114118
raise InvalidParameterValue(self.name, exc)
115119

openapi_core/schema/schemas/models.py

+26-14
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class Schema(object):
3838
"""Represents an OpenAPI Schema."""
3939

4040
DEFAULT_CAST_CALLABLE_GETTER = {
41-
SchemaType.INTEGER: int,
4241
SchemaType.NUMBER: float,
4342
SchemaType.BOOLEAN: forcebool,
4443
}
@@ -148,27 +147,29 @@ def get_all_required_properties_names(self):
148147

149148
return set(required)
150149

151-
def get_cast_mapping(self, custom_formatters=None):
150+
def get_cast_mapping(self, custom_formatters=None, strict=True):
152151
pass_defaults = lambda f: functools.partial(
153-
f, custom_formatters=custom_formatters)
152+
f, custom_formatters=custom_formatters, strict=strict)
154153
mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy()
155154
mapping.update({
156155
SchemaType.STRING: pass_defaults(self._unmarshal_string),
156+
SchemaType.INTEGER: pass_defaults(self._unmarshal_integer),
157157
SchemaType.ANY: pass_defaults(self._unmarshal_any),
158158
SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
159159
SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
160160
})
161161

162162
return defaultdict(lambda: lambda x: x, mapping)
163163

164-
def cast(self, value, custom_formatters=None):
164+
def cast(self, value, custom_formatters=None, strict=True):
165165
"""Cast value to schema type"""
166166
if value is None:
167167
if not self.nullable:
168168
raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type)
169169
return self.default
170170

171-
cast_mapping = self.get_cast_mapping(custom_formatters=custom_formatters)
171+
cast_mapping = self.get_cast_mapping(
172+
custom_formatters=custom_formatters, strict=strict)
172173

173174
if self.type is not SchemaType.STRING and value == '':
174175
return None
@@ -180,12 +181,12 @@ def cast(self, value, custom_formatters=None):
180181
raise InvalidSchemaValue(
181182
"Failed to cast value {value} to type {type}", value, self.type)
182183

183-
def unmarshal(self, value, custom_formatters=None):
184+
def unmarshal(self, value, custom_formatters=None, strict=True):
184185
"""Unmarshal parameter from the value."""
185186
if self.deprecated:
186187
warnings.warn("The schema is deprecated", DeprecationWarning)
187188

188-
casted = self.cast(value, custom_formatters=custom_formatters)
189+
casted = self.cast(value, custom_formatters=custom_formatters, strict=strict)
189190

190191
if casted is None and not self.required:
191192
return None
@@ -196,7 +197,10 @@ def unmarshal(self, value, custom_formatters=None):
196197

197198
return casted
198199

199-
def _unmarshal_string(self, value, custom_formatters=None):
200+
def _unmarshal_string(self, value, custom_formatters=None, strict=True):
201+
if strict and not isinstance(value, (text_type, binary_type)):
202+
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
203+
200204
try:
201205
schema_format = SchemaFormat(self.format)
202206
except ValueError:
@@ -216,7 +220,13 @@ def _unmarshal_string(self, value, custom_formatters=None):
216220
raise InvalidCustomFormatSchemaValue(
217221
"Failed to format value {value} to format {type}: {exception}", value, self.format, exc)
218222

219-
def _unmarshal_any(self, value, custom_formatters=None):
223+
def _unmarshal_integer(self, value, custom_formatters=None, strict=True):
224+
if strict and not isinstance(value, (integer_types, )):
225+
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
226+
227+
return int(value)
228+
229+
def _unmarshal_any(self, value, custom_formatters=None, strict=True):
220230
types_resolve_order = [
221231
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
222232
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
@@ -232,16 +242,18 @@ def _unmarshal_any(self, value, custom_formatters=None):
232242

233243
raise NoValidSchema(value)
234244

235-
def _unmarshal_collection(self, value, custom_formatters=None):
245+
def _unmarshal_collection(self, value, custom_formatters=None, strict=True):
236246
if self.items is None:
237247
raise UndefinedItemsSchema(self.type)
238248

239-
f = functools.partial(self.items.unmarshal,
240-
custom_formatters=custom_formatters)
249+
f = functools.partial(
250+
self.items.unmarshal,
251+
custom_formatters=custom_formatters, strict=strict,
252+
)
241253
return list(map(f, value))
242254

243255
def _unmarshal_object(self, value, model_factory=None,
244-
custom_formatters=None):
256+
custom_formatters=None, strict=True):
245257
if not isinstance(value, (dict, )):
246258
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
247259

@@ -270,7 +282,7 @@ def _unmarshal_object(self, value, model_factory=None,
270282
return model_factory.create(properties, name=self.model)
271283

272284
def _unmarshal_properties(self, value, one_of_schema=None,
273-
custom_formatters=None):
285+
custom_formatters=None, strict=True):
274286
all_props = self.get_all_properties()
275287
all_props_names = self.get_all_properties_names()
276288
all_req_props_names = self.get_all_required_properties_names()

tests/integration/data/v3.0/petstore.yaml

+18-16
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,7 @@ paths:
5959
explode: false
6060
responses:
6161
'200':
62-
description: An paged array of pets
63-
headers:
64-
x-next:
65-
description: A link to the next page of responses
66-
schema:
67-
type: string
68-
content:
69-
application/json:
70-
schema:
71-
$ref: "#/components/schemas/PetsData"
62+
$ref: "#/components/responses/PetsResponse"
7263
post:
7364
summary: Create a pet
7465
operationId: createPets
@@ -327,9 +318,20 @@ components:
327318
additionalProperties:
328319
type: string
329320
responses:
330-
ErrorResponse:
331-
description: unexpected error
332-
content:
333-
application/json:
334-
schema:
335-
$ref: "#/components/schemas/ExtendedError"
321+
ErrorResponse:
322+
description: unexpected error
323+
content:
324+
application/json:
325+
schema:
326+
$ref: "#/components/schemas/ExtendedError"
327+
PetsResponse:
328+
description: An paged array of pets
329+
headers:
330+
x-next:
331+
description: A link to the next page of responses
332+
schema:
333+
type: string
334+
content:
335+
application/json:
336+
schema:
337+
$ref: "#/components/schemas/PetsData"

tests/integration/test_petstore.py

+104-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
from openapi_core.schema.paths.models import Path
1818
from openapi_core.schema.request_bodies.models import RequestBody
1919
from openapi_core.schema.responses.models import Response
20+
from openapi_core.schema.schemas.enums import SchemaType
2021
from openapi_core.schema.schemas.exceptions import (
21-
NoValidSchema,
22+
NoValidSchema, InvalidSchemaProperty, InvalidSchemaValue,
2223
)
2324
from openapi_core.schema.schemas.models import Schema
2425
from openapi_core.schema.servers.exceptions import InvalidServer
@@ -234,6 +235,105 @@ def test_get_pets(self, spec, response_validator):
234235
assert isinstance(response_result.data, BaseModel)
235236
assert response_result.data.data == []
236237

238+
def test_get_pets_response(self, spec, response_validator):
239+
host_url = 'http://petstore.swagger.io/v1'
240+
path_pattern = '/v1/pets'
241+
query_params = {
242+
'limit': '20',
243+
}
244+
245+
request = MockRequest(
246+
host_url, 'GET', '/pets',
247+
path_pattern=path_pattern, args=query_params,
248+
)
249+
250+
parameters = request.get_parameters(spec)
251+
body = request.get_body(spec)
252+
253+
assert parameters == {
254+
'query': {
255+
'limit': 20,
256+
'page': 1,
257+
'search': '',
258+
}
259+
}
260+
assert body is None
261+
262+
data_json = {
263+
'data': [
264+
{
265+
'id': 1,
266+
'name': 'Cat',
267+
}
268+
],
269+
}
270+
data = json.dumps(data_json)
271+
response = MockResponse(data)
272+
273+
response_result = response_validator.validate(request, response)
274+
275+
assert response_result.errors == []
276+
assert isinstance(response_result.data, BaseModel)
277+
assert len(response_result.data.data) == 1
278+
assert response_result.data.data[0].id == 1
279+
assert response_result.data.data[0].name == 'Cat'
280+
281+
def test_get_pets_invalid_response(self, spec, response_validator):
282+
host_url = 'http://petstore.swagger.io/v1'
283+
path_pattern = '/v1/pets'
284+
query_params = {
285+
'limit': '20',
286+
}
287+
288+
request = MockRequest(
289+
host_url, 'GET', '/pets',
290+
path_pattern=path_pattern, args=query_params,
291+
)
292+
293+
parameters = request.get_parameters(spec)
294+
body = request.get_body(spec)
295+
296+
assert parameters == {
297+
'query': {
298+
'limit': 20,
299+
'page': 1,
300+
'search': '',
301+
}
302+
}
303+
assert body is None
304+
305+
data_json = {
306+
'data': [
307+
{
308+
'id': 1,
309+
'name': {
310+
'first_name': 'Cat',
311+
},
312+
}
313+
],
314+
}
315+
data = json.dumps(data_json)
316+
response = MockResponse(data)
317+
318+
response_result = response_validator.validate(request, response)
319+
320+
assert response_result.errors == [
321+
InvalidMediaTypeValue(
322+
original_exception=InvalidSchemaProperty(
323+
property_name='data',
324+
original_exception=InvalidSchemaProperty(
325+
property_name='name',
326+
original_exception=InvalidSchemaValue(
327+
msg="Value {value} is not of type {type}",
328+
type=SchemaType.STRING,
329+
value={'first_name': 'Cat'},
330+
),
331+
),
332+
),
333+
),
334+
]
335+
assert response_result.data is None
336+
237337
def test_get_pets_ids_param(self, spec, response_validator):
238338
host_url = 'http://petstore.swagger.io/v1'
239339
path_pattern = '/v1/pets'
@@ -419,7 +519,7 @@ def test_post_birds(self, spec, spec_dict):
419519
data_json = {
420520
'name': pet_name,
421521
'tag': pet_tag,
422-
'position': '2',
522+
'position': 2,
423523
'address': {
424524
'street': pet_street,
425525
'city': pet_city,
@@ -479,7 +579,7 @@ def test_post_cats(self, spec, spec_dict):
479579
data_json = {
480580
'name': pet_name,
481581
'tag': pet_tag,
482-
'position': '2',
582+
'position': 2,
483583
'address': {
484584
'street': pet_street,
485585
'city': pet_city,
@@ -539,7 +639,7 @@ def test_post_cats_boolean_string(self, spec, spec_dict):
539639
data_json = {
540640
'name': pet_name,
541641
'tag': pet_tag,
542-
'position': '2',
642+
'position': 2,
543643
'address': {
544644
'street': pet_street,
545645
'city': pet_city,

tests/integration/test_validators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_post_pets(self, validator, spec_dict):
155155
data_json = {
156156
'name': pet_name,
157157
'tag': pet_tag,
158-
'position': '2',
158+
'position': 2,
159159
'address': {
160160
'street': pet_street,
161161
'city': pet_city,

0 commit comments

Comments
 (0)