Skip to content

Commit d4bd56e

Browse files
committed
Validate oneOf with discriminator
1 parent ad54522 commit d4bd56e

File tree

3 files changed

+116
-1
lines changed

3 files changed

+116
-1
lines changed

openapi_schema_validator/_validators.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,41 @@
11
from jsonschema._utils import find_additional_properties, extras_msg
2+
from jsonschema._validators import oneOf as _oneOf
3+
24
from jsonschema.exceptions import ValidationError, FormatError
35

46

7+
def handle_discriminator(validator, _, instance, schema):
8+
discriminator = schema['discriminator']
9+
prop_name = discriminator['propertyName']
10+
prop_value = instance.get(prop_name)
11+
if not prop_value:
12+
# instance is missing discriminator value, this is a schema error
13+
yield ValidationError(
14+
"%r does not contain discriminating property" % (instance,),
15+
context=[],
16+
)
17+
return
18+
19+
# FIXME: handle implicit refs and missing mapping field
20+
subschema = discriminator['mapping'].get(prop_value)
21+
if not subschema:
22+
yield ValidationError(
23+
"%r is not a valid discriminator value, expected one of %r" % (
24+
instance, discriminator['mapping'].keys()),
25+
context=[],
26+
)
27+
return
28+
29+
yield from validator.descend(instance, subschema)
30+
31+
32+
def oneOf(validator, oneOf, instance, schema):
33+
if 'discriminator' not in schema:
34+
yield from _oneOf(validator, oneOf, instance, schema)
35+
else:
36+
yield from handle_discriminator(validator, oneOf, instance, schema)
37+
38+
539
def type(validator, data_type, instance, schema):
640
if instance is None:
741
return

openapi_schema_validator/validators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# adjusted to OAS
2929
u"type": oas_validators.type,
3030
u"allOf": _validators.allOf,
31-
u"oneOf": _validators.oneOf,
31+
u"oneOf": oas_validators.oneOf,
3232
u"anyOf": _validators.anyOf,
3333
u"not": _validators.not_,
3434
u"items": oas_validators.items,

tests/integration/test_validators.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,87 @@ def test_oneof_required(self):
240240
assert result is None
241241

242242

243+
def test_oneof_discriminator(self):
244+
schema = {
245+
"$ref": "#/$defs/Route",
246+
"$defs": {
247+
"MountainHiking": {
248+
"type": "object",
249+
"properties": {
250+
"discipline": {
251+
"type": "string",
252+
"enum": ["mountain_hiking"]
253+
},
254+
"length": {
255+
"type": "integer",
256+
}
257+
},
258+
"required": ["discipline", "length"]
259+
},
260+
"AlpineClimbing": {
261+
"type": "object",
262+
"properties": {
263+
"discipline": {
264+
"type": "string",
265+
"enum": ["alpine_climbing"]
266+
},
267+
"height": {
268+
"type": "integer",
269+
},
270+
},
271+
"required": ["discipline", "height"]
272+
},
273+
"Route": {
274+
"oneOf": [
275+
{"$ref": "#/$defs/MountainHiking"},
276+
{"$ref": "#/$defs/AlpineClimbing"},
277+
]
278+
}
279+
}
280+
}
281+
282+
# use jsonschema validator when no discriminator is defined
283+
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
284+
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
285+
validator.validate({
286+
"something": "matching_none_of_the_schemas"
287+
})
288+
assert False
289+
290+
discriminator = {
291+
"propertyName": "discipline",
292+
"mapping": {
293+
"mountain_hiking": {"$ref": "#/$defs/MountainHiking"},
294+
"alpine_climbing": {"$ref": "#/$defs/AlpineClimbing"},
295+
}
296+
}
297+
schema['$defs']['Route']['discriminator'] = discriminator
298+
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
299+
with pytest.raises(ValidationError, match="does not contain discriminating property"):
300+
validator.validate({
301+
"something": "missing"
302+
})
303+
assert False
304+
305+
with pytest.raises(ValidationError, match="is not a valid discriminator value, expected one of"):
306+
result = validator.validate({
307+
"discipline": "other"
308+
})
309+
assert False
310+
311+
with pytest.raises(ValidationError, match="'bad_string' is not of type integer"):
312+
validator.validate({
313+
"discipline": "mountain_hiking",
314+
"length": "bad_string"
315+
})
316+
assert False
317+
318+
validator.validate({
319+
"discipline": "mountain_hiking",
320+
"length": 10
321+
})
322+
323+
243324
class TestOAS31ValidatorValidate(object):
244325
@pytest.mark.parametrize('schema_type', [
245326
'boolean', 'array', 'integer', 'number', 'string',

0 commit comments

Comments
 (0)