Skip to content

Add anyOf #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions openapi_core/schema/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
118 changes: 76 additions & 42 deletions openapi_core/unmarshalling/schemas/unmarshallers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -182,59 +190,51 @@ def unmarshal(self, value):
else:
return self._unmarshal_object(value)

def _clone(self, schema):
return ObjectUnmarshaller(
Copy link
Collaborator

@p1c2u p1c2u Sep 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sisp can you explain me please more why you create new unmarshaller with new schema but with old validator and formatter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I believe it's better to use the SchemaUnmarshallersFactory instead. Not sure why I hadn't thought of that at the time.

I've created a PR: #425

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

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
Expand All @@ -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


Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/unmarshalling/test_unmarshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
108 changes: 108 additions & 0 deletions tests/unit/unmarshalling/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down