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 = {