Skip to content

Commit e3ec0e4

Browse files
p1c2uallcapssisp
committed
Add any-of
Co-authored-by: Coen van der Kamp <coen@fourdigits.nl> Co-authored-by: Sigurd Spieckermann <sigurd.spieckermann@gmail.com>
1 parent e3da8d3 commit e3ec0e4

File tree

4 files changed

+266
-50
lines changed

4 files changed

+266
-50
lines changed

openapi_core/schema/schemas.py

-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Any
22
from typing import Dict
3-
from typing import Set
43

54
from openapi_core.spec import Spec
65

@@ -17,8 +16,3 @@ def get_all_properties(schema: Spec) -> Dict[str, Any]:
1716
properties_dict.update(subschema_props)
1817

1918
return properties_dict
20-
21-
22-
def get_all_properties_names(schema: Spec) -> Set[str]:
23-
all_properties = get_all_properties(schema)
24-
return set(all_properties.keys())

openapi_core/unmarshalling/schemas/unmarshallers.py

+81-44
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
from openapi_core.extensions.models.factories import ModelClassImporter
2121
from openapi_core.schema.schemas import get_all_properties
22-
from openapi_core.schema.schemas import get_all_properties_names
2322
from openapi_core.spec import Spec
2423
from openapi_core.unmarshalling.schemas.datatypes import FormattersDict
2524
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
@@ -201,6 +200,15 @@ def object_class_factory(self) -> ModelClassImporter:
201200
return ModelClassImporter()
202201

203202
def unmarshal(self, value: Any) -> Any:
203+
properties = self.unmarshal_raw(value)
204+
205+
model = self.schema.getkey("x-model")
206+
fields: Iterable[str] = properties and properties.keys() or []
207+
object_class = self.object_class_factory.create(fields, model=model)
208+
209+
return object_class(**properties)
210+
211+
def unmarshal_raw(self, value: Any) -> Any:
204212
try:
205213
value = self.formatter.unmarshal(value)
206214
except ValueError as exc:
@@ -209,65 +217,57 @@ def unmarshal(self, value: Any) -> Any:
209217
else:
210218
return self._unmarshal_object(value)
211219

220+
def _clone(self, schema: Spec) -> "ObjectUnmarshaller":
221+
return ObjectUnmarshaller(
222+
schema,
223+
self.validator,
224+
self.formatter,
225+
self.unmarshallers_factory,
226+
self.context,
227+
)
228+
212229
def _unmarshal_object(self, value: Any) -> Any:
230+
properties = {}
231+
213232
if "oneOf" in self.schema:
214-
properties = None
233+
one_of_properties = None
215234
for one_of_schema in self.schema / "oneOf":
216235
try:
217-
unmarshalled = self._unmarshal_properties(
218-
value, one_of_schema
236+
unmarshalled = self._clone(one_of_schema).unmarshal_raw(
237+
value
219238
)
220239
except (UnmarshalError, ValueError):
221240
pass
222241
else:
223-
if properties is not None:
242+
if one_of_properties is not None:
224243
log.warning("multiple valid oneOf schemas found")
225244
continue
226-
properties = unmarshalled
245+
one_of_properties = unmarshalled
227246

228-
if properties is None:
247+
if one_of_properties is None:
229248
log.warning("valid oneOf schema not found")
249+
else:
250+
properties.update(one_of_properties)
230251

231-
else:
232-
properties = self._unmarshal_properties(value)
233-
234-
model = self.schema.getkey("x-model")
235-
fields: Iterable[str] = properties and properties.keys() or []
236-
object_class = self.object_class_factory.create(fields, model=model)
237-
238-
return object_class(**properties)
239-
240-
def _unmarshal_properties(
241-
self, value: Any, one_of_schema: Optional[Spec] = None
242-
) -> Dict[str, Any]:
243-
all_props = get_all_properties(self.schema)
244-
all_props_names = get_all_properties_names(self.schema)
245-
246-
if one_of_schema is not None:
247-
all_props.update(get_all_properties(one_of_schema))
248-
all_props_names |= get_all_properties_names(one_of_schema)
249-
250-
value_props_names = list(value.keys())
251-
extra_props = set(value_props_names) - set(all_props_names)
252+
elif "anyOf" in self.schema:
253+
any_of_properties = None
254+
for any_of_schema in self.schema / "anyOf":
255+
try:
256+
unmarshalled = self._clone(any_of_schema).unmarshal_raw(
257+
value
258+
)
259+
except (UnmarshalError, ValueError):
260+
pass
261+
else:
262+
any_of_properties = unmarshalled
263+
break
252264

253-
properties: Dict[str, Any] = {}
254-
additional_properties = self.schema.getkey(
255-
"additionalProperties", True
256-
)
257-
if additional_properties is not False:
258-
# free-form object
259-
if additional_properties is True:
260-
additional_prop_schema = Spec.from_dict({})
261-
# defined schema
265+
if any_of_properties is None:
266+
log.warning("valid anyOf schema not found")
262267
else:
263-
additional_prop_schema = self.schema / "additionalProperties"
264-
for prop_name in extra_props:
265-
prop_value = value[prop_name]
266-
properties[prop_name] = self.unmarshallers_factory.create(
267-
additional_prop_schema
268-
)(prop_value)
268+
properties.update(any_of_properties)
269269

270-
for prop_name, prop in list(all_props.items()):
270+
for prop_name, prop in get_all_properties(self.schema).items():
271271
read_only = prop.getkey("readOnly", False)
272272
if self.context == UnmarshalContext.REQUEST and read_only:
273273
continue
@@ -285,6 +285,24 @@ def _unmarshal_properties(
285285
prop_value
286286
)
287287

288+
additional_properties = self.schema.getkey(
289+
"additionalProperties", True
290+
)
291+
if additional_properties is not False:
292+
# free-form object
293+
if additional_properties is True:
294+
additional_prop_schema = Spec.from_dict({})
295+
# defined schema
296+
else:
297+
additional_prop_schema = self.schema / "additionalProperties"
298+
additional_prop_unmarshaler = self.unmarshallers_factory.create(
299+
additional_prop_schema
300+
)
301+
for prop_name, prop_value in value.items():
302+
if prop_name in properties:
303+
continue
304+
properties[prop_name] = additional_prop_unmarshaler(prop_value)
305+
288306
return properties
289307

290308

@@ -304,6 +322,10 @@ def unmarshal(self, value: Any) -> Any:
304322
if one_of_schema:
305323
return self.unmarshallers_factory.create(one_of_schema)(value)
306324

325+
any_of_schema = self._get_any_of_schema(value)
326+
if any_of_schema:
327+
return self.unmarshallers_factory.create(any_of_schema)(value)
328+
307329
all_of_schema = self._get_all_of_schema(value)
308330
if all_of_schema:
309331
return self.unmarshallers_factory.create(all_of_schema)(value)
@@ -338,6 +360,21 @@ def _get_one_of_schema(self, value: Any) -> Optional[Spec]:
338360
return subschema
339361
return None
340362

363+
def _get_any_of_schema(self, value: Any) -> Optional[Spec]:
364+
if "anyOf" not in self.schema:
365+
return None
366+
367+
any_of_schemas = self.schema / "anyOf"
368+
for subschema in any_of_schemas:
369+
unmarshaller = self.unmarshallers_factory.create(subschema)
370+
try:
371+
unmarshaller.validate(value)
372+
except ValidateError:
373+
continue
374+
else:
375+
return subschema
376+
return None
377+
341378
def _get_all_of_schema(self, value: Any) -> Optional[Spec]:
342379
if "allOf" not in self.schema:
343380
return None

tests/unit/unmarshalling/test_unmarshal.py

+59
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,65 @@ def test_schema_any_one_of(self, unmarshaller_factory):
560560
spec = Spec.from_dict(schema)
561561
assert unmarshaller_factory(spec)(["hello"]) == ["hello"]
562562

563+
def test_schema_any_any_of(self, unmarshaller_factory):
564+
schema = {
565+
"anyOf": [
566+
{
567+
"type": "string",
568+
},
569+
{
570+
"type": "array",
571+
"items": {
572+
"type": "string",
573+
},
574+
},
575+
],
576+
}
577+
spec = Spec.from_dict(schema)
578+
assert unmarshaller_factory(spec)(["hello"]) == ["hello"]
579+
580+
def test_schema_object_any_of(self, unmarshaller_factory):
581+
schema = {
582+
"type": "object",
583+
"anyOf": [
584+
{
585+
"type": "object",
586+
"required": ["someint"],
587+
"properties": {"someint": {"type": "integer"}},
588+
},
589+
{
590+
"type": "object",
591+
"required": ["somestr"],
592+
"properties": {"somestr": {"type": "string"}},
593+
},
594+
],
595+
}
596+
spec = Spec.from_dict(schema)
597+
result = unmarshaller_factory(spec)({"someint": 1})
598+
599+
assert is_dataclass(result)
600+
assert result.someint == 1
601+
602+
def test_schema_object_any_of_invalid(self, unmarshaller_factory):
603+
schema = {
604+
"type": "object",
605+
"anyOf": [
606+
{
607+
"type": "object",
608+
"required": ["someint"],
609+
"properties": {"someint": {"type": "integer"}},
610+
},
611+
{
612+
"type": "object",
613+
"required": ["somestr"],
614+
"properties": {"somestr": {"type": "string"}},
615+
},
616+
],
617+
}
618+
spec = Spec.from_dict(schema)
619+
with pytest.raises(UnmarshalError):
620+
unmarshaller_factory(spec)({"someint": "1"})
621+
563622
def test_schema_any_all_of(self, unmarshaller_factory):
564623
schema = {
565624
"allOf": [

tests/unit/unmarshalling/test_validate.py

+126
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,132 @@ def test_unambiguous_one_of(self, value, validator_factory):
863863

864864
assert result is None
865865

866+
@pytest.mark.parametrize(
867+
"value",
868+
[
869+
{},
870+
],
871+
)
872+
def test_object_multiple_any_of(self, value, validator_factory):
873+
any_of = [
874+
{
875+
"type": "object",
876+
},
877+
{
878+
"type": "object",
879+
},
880+
]
881+
schema = {
882+
"type": "object",
883+
"anyOf": any_of,
884+
}
885+
spec = Spec.from_dict(schema)
886+
887+
result = validator_factory(spec).validate(value)
888+
889+
assert result is None
890+
891+
@pytest.mark.parametrize(
892+
"value",
893+
[
894+
{},
895+
],
896+
)
897+
def test_object_different_type_any_of(self, value, validator_factory):
898+
any_of = [{"type": "integer"}, {"type": "string"}]
899+
schema = {
900+
"type": "object",
901+
"anyOf": any_of,
902+
}
903+
spec = Spec.from_dict(schema)
904+
905+
with pytest.raises(InvalidSchemaValue):
906+
validator_factory(spec).validate(value)
907+
908+
@pytest.mark.parametrize(
909+
"value",
910+
[
911+
{},
912+
],
913+
)
914+
def test_object_no_any_of(self, value, validator_factory):
915+
any_of = [
916+
{
917+
"type": "object",
918+
"required": ["test1"],
919+
"properties": {
920+
"test1": {
921+
"type": "string",
922+
},
923+
},
924+
},
925+
{
926+
"type": "object",
927+
"required": ["test2"],
928+
"properties": {
929+
"test2": {
930+
"type": "string",
931+
},
932+
},
933+
},
934+
]
935+
schema = {
936+
"type": "object",
937+
"anyOf": any_of,
938+
}
939+
spec = Spec.from_dict(schema)
940+
941+
with pytest.raises(InvalidSchemaValue):
942+
validator_factory(spec).validate(value)
943+
944+
@pytest.mark.parametrize(
945+
"value",
946+
[
947+
{
948+
"foo": "FOO",
949+
},
950+
{
951+
"foo": "FOO",
952+
"bar": "BAR",
953+
},
954+
],
955+
)
956+
def test_unambiguous_any_of(self, value, validator_factory):
957+
any_of = [
958+
{
959+
"type": "object",
960+
"required": ["foo"],
961+
"properties": {
962+
"foo": {
963+
"type": "string",
964+
},
965+
},
966+
"additionalProperties": False,
967+
},
968+
{
969+
"type": "object",
970+
"required": ["foo", "bar"],
971+
"properties": {
972+
"foo": {
973+
"type": "string",
974+
},
975+
"bar": {
976+
"type": "string",
977+
},
978+
},
979+
"additionalProperties": False,
980+
},
981+
]
982+
schema = {
983+
"type": "object",
984+
"anyOf": any_of,
985+
}
986+
spec = Spec.from_dict(schema)
987+
988+
result = validator_factory(spec).validate(value)
989+
990+
assert result is None
991+
866992
@pytest.mark.parametrize(
867993
"value",
868994
[

0 commit comments

Comments
 (0)