diff --git a/openapi_core/schema/schemas.py b/openapi_core/schema/schemas.py index 43919cb3..2a696adf 100644 --- a/openapi_core/schema/schemas.py +++ b/openapi_core/schema/schemas.py @@ -10,8 +10,3 @@ def get_all_properties(schema): properties_dict.update(subschema_props) return properties_dict - - -def get_all_properties_names(schema): - all_properties = get_all_properties(schema) - return set(all_properties.keys()) diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py index ba02ef23..4f2bbeed 100644 --- a/openapi_core/unmarshalling/schemas/unmarshallers.py +++ b/openapi_core/unmarshalling/schemas/unmarshallers.py @@ -10,9 +10,7 @@ from openapi_schema_validator._format import oas30_format_checker from openapi_core.extensions.models.factories import ModelFactory -from openapi_core.schema.schemas import ( - get_all_properties, get_all_properties_names -) +from openapi_core.schema.schemas import get_all_properties from openapi_core.unmarshalling.schemas.enums import UnmarshalContext from openapi_core.unmarshalling.schemas.exceptions import ( UnmarshalError, ValidateError, InvalidSchemaValue, @@ -174,6 +172,16 @@ def model_factory(self): return ModelFactory() def unmarshal(self, value): + properties = self.unmarshal_raw(value) + + if 'x-model' in self.schema: + name = self.schema['x-model'] + model = self.model_factory.create(properties, name=name) + return model + + return properties + + def unmarshal_raw(self, value): try: value = self.formatter.unmarshal(value) except ValueError as exc: @@ -182,59 +190,51 @@ def unmarshal(self, value): else: return self._unmarshal_object(value) + def _clone(self, schema): + return ObjectUnmarshaller( + schema, self.formatter, self.validator, self.unmarshallers_factory, + self.context) + def _unmarshal_object(self, value): + properties = {} + if 'oneOf' in self.schema: - properties = None + one_of_properties = None for one_of_schema in self.schema / 'oneOf': try: - unmarshalled = self._unmarshal_properties( - value, one_of_schema) + unmarshalled = self._clone(one_of_schema).unmarshal_raw( + value) except (UnmarshalError, ValueError): pass else: - if properties is not None: + if one_of_properties is not None: log.warning("multiple valid oneOf schemas found") continue - properties = unmarshalled + one_of_properties = unmarshalled - if properties is None: + if one_of_properties is None: log.warning("valid oneOf schema not found") + else: + properties.update(one_of_properties) - else: - properties = self._unmarshal_properties(value) - - if 'x-model' in self.schema: - name = self.schema['x-model'] - return self.model_factory.create(properties, name=name) - - return properties - - def _unmarshal_properties(self, value, one_of_schema=None): - all_props = get_all_properties(self.schema) - all_props_names = get_all_properties_names(self.schema) - - if one_of_schema is not None: - all_props.update(get_all_properties(one_of_schema)) - all_props_names |= get_all_properties_names(one_of_schema) - - value_props_names = list(value.keys()) - extra_props = set(value_props_names) - set(all_props_names) + elif 'anyOf' in self.schema: + any_of_properties = None + for any_of_schema in self.schema / 'anyOf': + try: + unmarshalled = self._clone(any_of_schema).unmarshal_raw( + value) + except (UnmarshalError, ValueError): + pass + else: + any_of_properties = unmarshalled + break - properties = {} - additional_properties = self.schema.getkey( - 'additionalProperties', True) - if isinstance(additional_properties, dict): - additional_prop_schema = self.schema / 'additionalProperties' - for prop_name in extra_props: - prop_value = value[prop_name] - properties[prop_name] = self.unmarshallers_factory.create( - additional_prop_schema)(prop_value) - elif additional_properties is True: - for prop_name in extra_props: - prop_value = value[prop_name] - properties[prop_name] = prop_value + if any_of_properties is None: + log.warning("valid anyOf schema not found") + else: + properties.update(any_of_properties) - for prop_name, prop in list(all_props.items()): + for prop_name, prop in get_all_properties(self.schema).items(): read_only = prop.getkey('readOnly', False) if self.context == UnmarshalContext.REQUEST and read_only: continue @@ -251,6 +251,22 @@ def _unmarshal_properties(self, value, one_of_schema=None): properties[prop_name] = self.unmarshallers_factory.create( prop)(prop_value) + additional_properties = self.schema.getkey( + 'additionalProperties', True) + if isinstance(additional_properties, dict): + additional_prop_schema = self.schema / 'additionalProperties' + additional_prop_unmarshaler = self.unmarshallers_factory.create( + additional_prop_schema) + for prop_name, prop_value in value.items(): + if prop_name in properties: + continue + properties[prop_name] = additional_prop_unmarshaler(prop_value) + elif additional_properties is True: + for prop_name, prop_value in value.items(): + if prop_name in properties: + continue + properties[prop_name] = prop_value + return properties @@ -266,6 +282,10 @@ def unmarshal(self, value): if one_of_schema: return self.unmarshallers_factory.create(one_of_schema)(value) + any_of_schema = self._get_any_of_schema(value) + if any_of_schema: + return self.unmarshallers_factory.create(any_of_schema)(value) + all_of_schema = self._get_all_of_schema(value) if all_of_schema: return self.unmarshallers_factory.create(all_of_schema)(value) @@ -298,6 +318,20 @@ def _get_one_of_schema(self, value): else: return subschema + def _get_any_of_schema(self, value): + if 'anyOf' not in self.schema: + return + + any_of_schemas = self.schema / 'anyOf' + for subschema in any_of_schemas: + unmarshaller = self.unmarshallers_factory.create(subschema) + try: + unmarshaller.validate(value) + except ValidateError: + continue + else: + return subschema + def _get_all_of_schema(self, value): if 'allOf' not in self.schema: return diff --git a/tests/unit/unmarshalling/test_unmarshal.py b/tests/unit/unmarshalling/test_unmarshal.py index 8d88b1f0..5e117af8 100644 --- a/tests/unit/unmarshalling/test_unmarshal.py +++ b/tests/unit/unmarshalling/test_unmarshal.py @@ -542,6 +542,78 @@ def test_schema_any_one_of(self, unmarshaller_factory): schema = SpecPath.from_spec(spec) assert unmarshaller_factory(schema)(['hello']) == ['hello'] + def test_schema_any_any_of(self, unmarshaller_factory): + spec = { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'type': 'array', + 'items': { + 'type': 'string', + } + } + ], + } + schema = SpecPath.from_spec(spec) + assert unmarshaller_factory(schema)(['hello']) == ['hello'] + + def test_schema_object_any_of(self, unmarshaller_factory): + spec = { + 'type': 'object', + 'anyOf': [ + { + 'type': 'object', + 'required': ['someint'], + 'properties': { + 'someint': { + 'type': 'integer' + } + } + }, + { + 'type': 'object', + 'required': ['somestr'], + 'properties': { + 'somestr': { + 'type': 'string' + } + } + } + ], + } + schema = SpecPath.from_spec(spec) + assert unmarshaller_factory(schema)({'someint': 1}) == {'someint': 1} + + def test_schema_object_any_of_invalid(self, unmarshaller_factory): + spec = { + 'type': 'object', + 'anyOf': [ + { + 'type': 'object', + 'required': ['someint'], + 'properties': { + 'someint': { + 'type': 'integer' + } + } + }, + { + 'type': 'object', + 'required': ['somestr'], + 'properties': { + 'somestr': { + 'type': 'string' + } + } + } + ], + } + schema = SpecPath.from_spec(spec) + with pytest.raises(UnmarshalError): + unmarshaller_factory(schema)({'someint': '1'}) + def test_schema_any_all_of(self, unmarshaller_factory): spec = { 'allOf': [ diff --git a/tests/unit/unmarshalling/test_validate.py b/tests/unit/unmarshalling/test_validate.py index c867b851..cc9b8274 100644 --- a/tests/unit/unmarshalling/test_validate.py +++ b/tests/unit/unmarshalling/test_validate.py @@ -761,6 +761,114 @@ def test_unambiguous_one_of(self, value, validator_factory): assert result is None + @pytest.mark.parametrize('value', [{}, ]) + def test_object_multiple_any_of(self, value, validator_factory): + any_of = [ + { + 'type': 'object', + }, + { + 'type': 'object', + }, + ] + spec = { + 'type': 'object', + 'anyOf': any_of, + } + schema = SpecPath.from_spec(spec) + + result = validator_factory(schema).validate(value) + + assert result is None + + @pytest.mark.parametrize('value', [{}, ]) + def test_object_different_type_any_of(self, value, validator_factory): + any_of = [{'type': 'integer'}, {'type': 'string'}] + spec = { + 'type': 'object', + 'anyOf': any_of, + } + schema = SpecPath.from_spec(spec) + + with pytest.raises(InvalidSchemaValue): + validator_factory(schema).validate(value) + + @pytest.mark.parametrize('value', [{}, ]) + def test_object_no_any_of(self, value, validator_factory): + any_of = [ + { + 'type': 'object', + 'required': ['test1'], + 'properties': { + 'test1': { + 'type': 'string', + }, + }, + }, + { + 'type': 'object', + 'required': ['test2'], + 'properties': { + 'test2': { + 'type': 'string', + }, + }, + } + ] + spec = { + 'type': 'object', + 'anyOf': any_of, + } + schema = SpecPath.from_spec(spec) + + with pytest.raises(InvalidSchemaValue): + validator_factory(schema).validate(value) + + @pytest.mark.parametrize('value', [ + { + 'foo': 'FOO', + }, + { + 'foo': 'FOO', + 'bar': 'BAR', + }, + ]) + def test_unambiguous_any_of(self, value, validator_factory): + any_of = [ + { + 'type': 'object', + 'required': ['foo'], + 'properties': { + 'foo': { + 'type': 'string', + }, + }, + 'additionalProperties': False, + }, + { + 'type': 'object', + 'required': ['foo', 'bar'], + 'properties': { + 'foo': { + 'type': 'string', + }, + 'bar': { + 'type': 'string', + }, + }, + 'additionalProperties': False, + }, + ] + spec = { + 'type': 'object', + 'anyOf': any_of, + } + schema = SpecPath.from_spec(spec) + + result = validator_factory(schema).validate(value) + + assert result is None + @pytest.mark.parametrize('value', [{}, ]) def test_object_default_property(self, value, validator_factory): spec = {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: