Skip to content

Commit 1543afe

Browse files
committed
Fully handle anyOf, oneOf and allOf
- handle implicit refs and missing "mapping"; - test all 3 cases.
1 parent 346e999 commit 1543afe

File tree

3 files changed

+81
-32
lines changed

3 files changed

+81
-32
lines changed

Diff for: openapi_schema_validator/_validators.py

+32-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from jsonschema._utils import find_additional_properties, extras_msg
2-
from jsonschema._validators import oneOf as _oneOf
2+
from jsonschema._validators import oneOf as _oneOf, anyOf as _anyOf, allOf as _allOf
33

44
from jsonschema.exceptions import ValidationError, FormatError
55

66

77
def handle_discriminator(validator, _, instance, schema):
8+
"""
9+
Handle presence of discriminator in anyOf, oneOf and allOf.
10+
The behaviour is the same in all 3 cases because at most 1 schema will match.
11+
"""
812
discriminator = schema['discriminator']
913
prop_name = discriminator['propertyName']
1014
prop_value = instance.get(prop_name)
@@ -16,37 +20,54 @@ def handle_discriminator(validator, _, instance, schema):
1620
)
1721
return
1822

19-
# FIXME: handle implicit refs and missing mapping field
20-
explicitRef = discriminator['mapping'].get(prop_value)
21-
if not explicitRef:
23+
# Use explicit mapping if available, otherwise try implicit value
24+
ref = discriminator.get('mapping', {}).get(prop_value) or f'#/components/schemas/{prop_value}'
25+
26+
if not isinstance(ref, str):
27+
# this is a schema error
2228
yield ValidationError(
23-
"%r is not a valid discriminator value, expected one of %r" % (
24-
instance, discriminator['mapping'].keys()),
29+
"%r mapped value for %r should be a string, was %r" % (
30+
instance, prop_value, ref),
2531
context=[],
2632
)
2733
return
2834

29-
if not isinstance(explicitRef, str):
30-
# this is a schema error
35+
try:
36+
validator.resolver.resolve(ref)
37+
except:
3138
yield ValidationError(
32-
"%r mapped value for %r should be a string, was %r" % (
33-
instance, prop_value, explicitRef),
39+
"%r reference %r could not be resolved" % (
40+
instance, ref),
3441
context=[],
3542
)
3643
return
3744

3845
yield from validator.descend(instance, {
39-
"$ref": explicitRef
46+
"$ref": ref
4047
})
4148

4249

50+
def anyOf(validator, anyOf, instance, schema):
51+
if 'discriminator' not in schema:
52+
yield from _anyOf(validator, anyOf, instance, schema)
53+
else:
54+
yield from handle_discriminator(validator, anyOf, instance, schema)
55+
56+
4357
def oneOf(validator, oneOf, instance, schema):
4458
if 'discriminator' not in schema:
4559
yield from _oneOf(validator, oneOf, instance, schema)
4660
else:
4761
yield from handle_discriminator(validator, oneOf, instance, schema)
4862

4963

64+
def allOf(validator, allOf, instance, schema):
65+
if 'discriminator' not in schema:
66+
yield from _allOf(validator, allOf, instance, schema)
67+
else:
68+
yield from handle_discriminator(validator, allOf, instance, schema)
69+
70+
5071
def type(validator, data_type, instance, schema):
5172
if instance is None:
5273
return

Diff for: openapi_schema_validator/validators.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
u"enum": _validators.enum,
2828
# adjusted to OAS
2929
u"type": oas_validators.type,
30-
u"allOf": _validators.allOf,
30+
u"allOf": oas_validators.allOf,
3131
u"oneOf": oas_validators.oneOf,
32-
u"anyOf": _validators.anyOf,
32+
u"anyOf": oas_validators.anyOf,
3333
u"not": _validators.not_,
3434
u"items": oas_validators.items,
3535
u"properties": _validators.properties,

Diff for: tests/integration/test_validators.py

+47-19
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,20 @@ def test_oneof_required(self):
240240
assert result is None
241241

242242

243-
def test_oneof_discriminator(self):
243+
@pytest.mark.parametrize('schema_type', [
244+
'oneOf', 'anyOf', 'allOf',
245+
])
246+
def test_oneof_discriminator(self, schema_type):
244247
# We define a few components schemas
245248
components = {
246249
"MountainHiking": {
247250
"type": "object",
248251
"properties": {
249252
"discipline": {
250253
"type": "string",
251-
"enum": ["mountain_hiking"]
254+
# we allow both the explicitely matched mountain_hiking discipline
255+
# and the implicitely matched MoutainHiking discipline
256+
"enum": ["mountain_hiking", "MountainHiking"]
252257
},
253258
"length": {
254259
"type": "integer",
@@ -270,12 +275,13 @@ def test_oneof_discriminator(self):
270275
"required": ["discipline", "height"]
271276
},
272277
"Route": {
273-
"oneOf": [
274-
{"$ref": "#/components/schemas/MountainHiking"},
275-
{"$ref": "#/components/schemas/AlpineClimbing"},
276-
]
278+
# defined later
277279
}
278280
}
281+
components['Route'][schema_type] = [
282+
{"$ref": "#/components/schemas/MountainHiking"},
283+
{"$ref": "#/components/schemas/AlpineClimbing"},
284+
]
279285

280286
# Add the compoments in a minimalis schema
281287
schema = {
@@ -285,13 +291,23 @@ def test_oneof_discriminator(self):
285291
}
286292
}
287293

288-
# use jsonschema validator when no discriminator is defined
289-
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
290-
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
291-
validator.validate({
292-
"something": "matching_none_of_the_schemas"
293-
})
294-
assert False
294+
if schema_type != 'allOf':
295+
# use jsonschema validator when no discriminator is defined
296+
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
297+
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
298+
validator.validate({
299+
"something": "matching_none_of_the_schemas"
300+
})
301+
assert False
302+
303+
if schema_type == 'anyOf':
304+
# use jsonschema validator when no discriminator is defined
305+
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
306+
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
307+
validator.validate({
308+
"something": "matching_none_of_the_schemas"
309+
})
310+
assert False
295311

296312
discriminator = {
297313
"propertyName": "discipline",
@@ -301,31 +317,43 @@ def test_oneof_discriminator(self):
301317
}
302318
}
303319
schema['components']['schemas']['Route']['discriminator'] = discriminator
320+
321+
# Optional: check we return useful result when the schema is wrong
304322
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
305323
with pytest.raises(ValidationError, match="does not contain discriminating property"):
306324
validator.validate({
307325
"something": "missing"
308326
})
309327
assert False
310328

311-
with pytest.raises(ValidationError, match="is not a valid discriminator value, expected one of"):
312-
result = validator.validate({
313-
"discipline": "other"
314-
})
315-
assert False
316-
329+
# Check we get a non-generic, somehow usable, error message when a discriminated schema is failing
317330
with pytest.raises(ValidationError, match="'bad_string' is not of type integer"):
318331
validator.validate({
319332
"discipline": "mountain_hiking",
320333
"length": "bad_string"
321334
})
322335
assert False
323336

337+
# Check explicit MountainHiking resolution
324338
validator.validate({
325339
"discipline": "mountain_hiking",
326340
"length": 10
327341
})
328342

343+
# Check implicit MountainHiking resolution
344+
validator.validate({
345+
"discipline": "MountainHiking",
346+
"length": 10
347+
})
348+
349+
# Check non resolvable implicit schema
350+
with pytest.raises(ValidationError, match="reference '#/components/schemas/other' could not be resolved"):
351+
result = validator.validate({
352+
"discipline": "other"
353+
})
354+
assert False
355+
356+
329357

330358
class TestOAS31ValidatorValidate(object):
331359
@pytest.mark.parametrize('schema_type', [

0 commit comments

Comments
 (0)