Skip to content

Validate oneOf, anyOf and allOf with discriminator #30

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

Merged
merged 3 commits into from
Jan 27, 2022
Merged
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
66 changes: 66 additions & 0 deletions openapi_schema_validator/_validators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,73 @@
from jsonschema._utils import find_additional_properties, extras_msg
from jsonschema._validators import oneOf as _oneOf, anyOf as _anyOf, allOf as _allOf

from jsonschema.exceptions import ValidationError, FormatError


def handle_discriminator(validator, _, instance, schema):
"""
Handle presence of discriminator in anyOf, oneOf and allOf.
The behaviour is the same in all 3 cases because at most 1 schema will match.
"""
discriminator = schema['discriminator']
prop_name = discriminator['propertyName']
prop_value = instance.get(prop_name)
if not prop_value:
# instance is missing $propertyName
yield ValidationError(
"%r does not contain discriminating property %r" % (instance, prop_name),
context=[],
)
return

# Use explicit mapping if available, otherwise try implicit value
ref = discriminator.get('mapping', {}).get(prop_value) or f'#/components/schemas/{prop_value}'

if not isinstance(ref, str):
# this is a schema error
yield ValidationError(
"%r mapped value for %r should be a string, was %r" % (
instance, prop_value, ref),
context=[],
)
return

try:
validator.resolver.resolve(ref)
except:
yield ValidationError(
"%r reference %r could not be resolved" % (
instance, ref),
context=[],
)
return

yield from validator.descend(instance, {
"$ref": ref
})


def anyOf(validator, anyOf, instance, schema):
if 'discriminator' not in schema:
yield from _anyOf(validator, anyOf, instance, schema)
else:
yield from handle_discriminator(validator, anyOf, instance, schema)


def oneOf(validator, oneOf, instance, schema):
if 'discriminator' not in schema:
yield from _oneOf(validator, oneOf, instance, schema)
else:
yield from handle_discriminator(validator, oneOf, instance, schema)


def allOf(validator, allOf, instance, schema):
if 'discriminator' not in schema:
yield from _allOf(validator, allOf, instance, schema)
else:
yield from handle_discriminator(validator, allOf, instance, schema)


def type(validator, data_type, instance, schema):
if instance is None:
return
Expand Down
6 changes: 3 additions & 3 deletions openapi_schema_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
u"enum": _validators.enum,
# adjusted to OAS
u"type": oas_validators.type,
u"allOf": _validators.allOf,
u"oneOf": _validators.oneOf,
u"anyOf": _validators.anyOf,
u"allOf": oas_validators.allOf,
u"oneOf": oas_validators.oneOf,
u"anyOf": oas_validators.anyOf,
u"not": _validators.not_,
u"items": oas_validators.items,
u"properties": _validators.properties,
Expand Down
115 changes: 115 additions & 0 deletions tests/integration/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,121 @@ def test_oneof_required(self):
assert result is None


@pytest.mark.parametrize('schema_type', [
'oneOf', 'anyOf', 'allOf',
])
def test_oneof_discriminator(self, schema_type):
# We define a few components schemas
components = {
"MountainHiking": {
"type": "object",
"properties": {
"discipline": {
"type": "string",
# we allow both the explicitely matched mountain_hiking discipline
# and the implicitely matched MoutainHiking discipline
"enum": ["mountain_hiking", "MountainHiking"]
},
"length": {
"type": "integer",
}
},
"required": ["discipline", "length"]
},
"AlpineClimbing": {
"type": "object",
"properties": {
"discipline": {
"type": "string",
"enum": ["alpine_climbing"]
},
"height": {
"type": "integer",
},
},
"required": ["discipline", "height"]
},
"Route": {
# defined later
}
}
components['Route'][schema_type] = [
{"$ref": "#/components/schemas/MountainHiking"},
{"$ref": "#/components/schemas/AlpineClimbing"},
]

# Add the compoments in a minimalis schema
schema = {
"$ref": "#/components/schemas/Route",
"components": {
"schemas": components
}
}

if schema_type != 'allOf':
# use jsonschema validator when no discriminator is defined
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
validator.validate({
"something": "matching_none_of_the_schemas"
})
assert False

if schema_type == 'anyOf':
# use jsonschema validator when no discriminator is defined
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
with pytest.raises(ValidationError, match="is not valid under any of the given schemas"):
validator.validate({
"something": "matching_none_of_the_schemas"
})
assert False

discriminator = {
"propertyName": "discipline",
"mapping": {
"mountain_hiking": "#/components/schemas/MountainHiking",
"alpine_climbing": "#/components/schemas/AlpineClimbing",
}
}
schema['components']['schemas']['Route']['discriminator'] = discriminator

# Optional: check we return useful result when the schema is wrong
validator = OAS30Validator(schema, format_checker=oas30_format_checker)
with pytest.raises(ValidationError, match="does not contain discriminating property"):
validator.validate({
"something": "missing"
})
assert False

# Check we get a non-generic, somehow usable, error message when a discriminated schema is failing
with pytest.raises(ValidationError, match="'bad_string' is not of type integer"):
validator.validate({
"discipline": "mountain_hiking",
"length": "bad_string"
})
assert False

# Check explicit MountainHiking resolution
validator.validate({
"discipline": "mountain_hiking",
"length": 10
})

# Check implicit MountainHiking resolution
validator.validate({
"discipline": "MountainHiking",
"length": 10
})

# Check non resolvable implicit schema
with pytest.raises(ValidationError, match="reference '#/components/schemas/other' could not be resolved"):
result = validator.validate({
"discipline": "other"
})
assert False



class TestOAS31ValidatorValidate(object):
@pytest.mark.parametrize('schema_type', [
'boolean', 'array', 'integer', 'number', 'string',
Expand Down