diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py index 55b48fe1..0ffb02eb 100644 --- a/openapi_core/schema/schemas/factories.py +++ b/openapi_core/schema/schemas/factories.py @@ -31,6 +31,7 @@ def create(self, schema_spec): deprecated = schema_deref.get('deprecated', False) all_of_spec = schema_deref.get('allOf', None) one_of_spec = schema_deref.get('oneOf', None) + any_of_spec = schema_deref.get('anyOf', None) additional_properties_spec = schema_deref.get('additionalProperties', True) min_items = schema_deref.get('minItems', None) @@ -63,6 +64,10 @@ def create(self, schema_spec): if one_of_spec: one_of = list(map(self.create, one_of_spec)) + any_of = [] + if any_of_spec: + any_of = list(map(self.create, any_of_spec)) + items = None if items_spec: items = self._create_items(items_spec) @@ -75,7 +80,7 @@ def create(self, schema_spec): schema_type=schema_type, properties=properties, items=items, schema_format=schema_format, required=required, default=default, nullable=nullable, enum=enum, - deprecated=deprecated, all_of=all_of, one_of=one_of, + deprecated=deprecated, all_of=all_of, one_of=one_of, any_of=any_of, additional_properties=additional_properties, min_items=min_items, max_items=max_items, min_length=min_length, max_length=max_length, pattern=pattern, unique_items=unique_items, @@ -118,6 +123,10 @@ class SchemaDictFactory(object): 'one_of', dest_prop_name='oneOf', is_list=True, dest_default=[], ), + Contribution( + 'any_of', + dest_prop_name='anyOf', is_list=True, dest_default=[], + ), Contribution( 'additional_properties', dest_prop_name='additionalProperties', dest_default=True, diff --git a/openapi_core/schema/schemas/models.py b/openapi_core/schema/schemas/models.py index a4109c4d..4a187ae4 100644 --- a/openapi_core/schema/schemas/models.py +++ b/openapi_core/schema/schemas/models.py @@ -21,7 +21,7 @@ class Schema(object): def __init__( self, schema_type=None, properties=None, items=None, schema_format=None, required=None, default=NoValue, nullable=False, - enum=None, deprecated=False, all_of=None, one_of=None, + enum=None, deprecated=False, all_of=None, one_of=None, any_of=None, additional_properties=True, min_items=None, max_items=None, min_length=None, max_length=None, pattern=None, unique_items=False, minimum=None, maximum=None, multiple_of=None, @@ -40,6 +40,7 @@ def __init__( self.deprecated = deprecated self.all_of = all_of and list(all_of) or [] self.one_of = one_of and list(one_of) or [] + self.any_of = any_of and list(any_of) or [] self.additional_properties = additional_properties self.min_items = int(min_items) if min_items is not None else None self.max_items = int(max_items) if max_items is not None else None diff --git a/openapi_core/unmarshalling/schemas/unmarshallers.py b/openapi_core/unmarshalling/schemas/unmarshallers.py index ef0fdb70..19c6a66b 100644 --- a/openapi_core/unmarshalling/schemas/unmarshallers.py +++ b/openapi_core/unmarshalling/schemas/unmarshallers.py @@ -187,6 +187,23 @@ def _unmarshal_object(self, value=NoValue): if properties is None: log.warning("valid oneOf schema not found") + if self.schema.any_of: + properties = None + for any_of_schema in self.schema.any_of: + try: + unmarshalled = self._unmarshal_properties( + value, any_of_schema) + except (UnmarshalError, ValueError): + pass + else: + if properties is not None: + log.warning("multiple valid anyOf schemas found") + continue + properties = unmarshalled + + if properties is None: + log.warning("valid anyOf schema not found") + else: properties = self._unmarshal_properties(value) @@ -196,7 +213,8 @@ def _unmarshal_object(self, value=NoValue): return properties - def _unmarshal_properties(self, value=NoValue, one_of_schema=None): + def _unmarshal_properties(self, value=NoValue, one_of_schema=None, + any_of_schema=None): all_props = self.schema.get_all_properties() all_props_names = self.schema.get_all_properties_names() @@ -205,6 +223,11 @@ def _unmarshal_properties(self, value=NoValue, one_of_schema=None): all_props_names |= one_of_schema.\ get_all_properties_names() + if any_of_schema is not None: + all_props.update(any_of_schema.get_all_properties()) + all_props_names |= any_of_schema.\ + get_all_properties_names() + value_props_names = value.keys() extra_props = set(value_props_names) - set(all_props_names) @@ -253,6 +276,10 @@ def __call__(self, value=NoValue): 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) @@ -283,6 +310,18 @@ def _get_one_of_schema(self, value): else: return subschema + def _get_any_of_schema(self, value): + if not self.schema.any_of: + return + for subschema in self.schema.any_of: + unmarshaller = self.unmarshallers_factory.create(subschema) + try: + unmarshaller.validate(value) + except ValidateError: + continue + else: + return subschema + def _get_all_of_schema(self, value): if not self.schema.all_of: return diff --git a/tests/unit/unmarshalling/test_validate.py b/tests/unit/unmarshalling/test_validate.py index fdb5d950..9dafb6a7 100644 --- a/tests/unit/unmarshalling/test_validate.py +++ b/tests/unit/unmarshalling/test_validate.py @@ -529,6 +529,80 @@ def test_unambiguous_one_of(self, value, validator_factory): assert result is None + @pytest.mark.parametrize('value', [Model(), ]) + def test_object_multiple_any_of(self, value, validator_factory): + any_of = [ + Schema('object'), Schema('object'), + ] + schema = Schema('object', any_of=any_of) + + with pytest.raises(InvalidSchemaValue): + validator_factory(schema).validate(value) + + @pytest.mark.parametrize('value', [{}, ]) + def test_object_different_type_any_of(self, value, validator_factory): + any_of = [ + Schema('integer'), Schema('string'), + ] + schema = Schema('object', any_of=any_of) + + 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 = [ + Schema( + 'object', + properties={'test1': Schema('string')}, + required=['test1', ], + ), + Schema( + 'object', + properties={'test2': Schema('string')}, + required=['test2', ], + ), + ] + schema = Schema('object', any_of=any_of) + + with pytest.raises(InvalidSchemaValue): + validator_factory(schema).validate(value) + + @pytest.mark.parametrize('value', [ + { + 'foo': u("FOO"), + }, + { + 'foo': u("FOO"), + 'bar': u("BAR"), + }, + ]) + def test_unambiguous_any_of(self, value, validator_factory): + any_of = [ + Schema( + 'object', + properties={ + 'foo': Schema('string'), + }, + additional_properties=False, + required=['foo'], + ), + Schema( + 'object', + properties={ + 'foo': Schema('string'), + 'bar': Schema('string'), + }, + additional_properties=False, + required=['foo', 'bar'], + ), + ] + schema = Schema('object', any_of=any_of) + + result = validator_factory(schema).validate(value) + + assert result is None + @pytest.mark.parametrize('value', [{}, ]) def test_object_default_property(self, value, validator_factory): schema = Schema('object', default='value1')