Skip to content

Fix #20 - nullable semantics with $ref, oneOf, anyOf, and allOf #57

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 2 commits into from
Jan 7, 2023
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
39 changes: 8 additions & 31 deletions openapi_schema_validator/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,6 @@
from jsonschema.protocols import Validator


def include_nullable_validator(
schema: Dict[Hashable, Any]
) -> ItemsView[Hashable, Any]:
"""
Include ``nullable`` validator always.
Suitable for use with `create`'s ``applicable_validators`` argument.
"""
_schema = deepcopy(schema)

# append defaults to trigger nullable validator
if "nullable" not in _schema:
_schema.update(
{
"nullable": False,
}
)

return _schema.items()


def handle_discriminator(
validator: Validator, _: Any, instance: Any, schema: Mapping[Hashable, Any]
) -> Iterator[ValidationError]:
Expand Down Expand Up @@ -127,7 +107,14 @@ def type(
schema: Mapping[Hashable, Any],
) -> Iterator[ValidationError]:
if instance is None:
return
# nullable implementation based on OAS 3.0.3
# * nullable is only meaningful if its value is true
# * nullable: true is only meaningful in combination with a type
# assertion specified in the same Schema Object.
# * nullable: true operates within a single Schema Object
if "nullable" in schema and schema["nullable"] == True:
return
yield ValidationError("None for not nullable")

if not validator.is_type(instance, data_type):
data_repr = repr(data_type)
Expand Down Expand Up @@ -163,16 +150,6 @@ def items(
yield from validator.descend(item, items, path=index)


def nullable(
validator: Validator,
is_nullable: bool,
instance: Any,
schema: Mapping[Hashable, Any],
) -> Iterator[ValidationError]:
if instance is None and not is_nullable:
yield ValidationError("None for not nullable")


def required(
validator: Validator,
required: List[str],
Expand Down
2 changes: 0 additions & 2 deletions openapi_schema_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
# TODO: adjust default
"$ref": _validators.ref,
# fixed OAS fields
"nullable": oas_validators.nullable,
"discriminator": oas_validators.not_implemented,
"readOnly": oas_validators.readOnly,
"writeOnly": oas_validators.writeOnly,
Expand All @@ -59,7 +58,6 @@
# See https://github.com/p1c2u/openapi-schema-validator/pull/12
# version="oas30",
id_of=lambda schema: schema.get("id", ""),
applicable_validators=oas_validators.include_nullable_validator,
)

OAS31Validator = extend(
Expand Down
124 changes: 124 additions & 0 deletions tests/integration/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ def test_null(self, schema_type):
with pytest.raises(ValidationError):
validator.validate(value)

@pytest.mark.parametrize("is_nullable", [True, False])
def test_nullable_untyped(self, is_nullable):
schema = {"nullable": is_nullable}
validator = OAS30Validator(schema)
value = None

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize(
"schema_type",
[
Expand All @@ -50,6 +60,23 @@ def test_nullable(self, schema_type):

assert result is None

def test_nullable_enum_without_none(self):
schema = {"type": "integer", "nullable": True, "enum": [1, 2, 3]}
validator = OAS30Validator(schema)
value = None

with pytest.raises(ValidationError):
validator.validate(value)

def test_nullable_enum_with_none(self):
schema = {"type": "integer", "nullable": True, "enum": [1, 2, 3, None]}
validator = OAS30Validator(schema)
value = None

result = validator.validate(value)

assert result is None

@pytest.mark.parametrize(
"value",
[
Expand Down Expand Up @@ -442,6 +469,103 @@ def test_oneof_discriminator(self, schema_type):
result = validator.validate({"discipline": "other"})
assert False

@pytest.mark.parametrize("is_nullable", [True, False])
def test_nullable_ref(self, is_nullable):
"""
Tests that a field that points to a schema reference is null checked based on the $ref schema rather than
on this schema
:param is_nullable: if the schema is marked as nullable. If not, validate an exception is raised on None
"""
schema = {
"$ref": "#/$defs/Pet",
"$defs": {
"NullableText": {
"type": "string",
"nullable": is_nullable
},
"Pet": {
"properties": {
"testfield": {"$ref": "#/$defs/NullableText"},
},
}
},
}
validator = OAS30Validator(
schema,
format_checker=oas30_format_checker,
)

result = validator.validate({"testfield": "John"})
assert result is None

if is_nullable:
result = validator.validate({"testfield": None})
assert result is None
else:
with pytest.raises(
ValidationError,
match="None for not nullable",
):
validator.validate({"testfield": None})
assert False


@pytest.mark.parametrize(
"schema_type, not_nullable_regex",
[
("oneOf", "None is not valid under any of the given schemas"),
("anyOf", "None is not valid under any of the given schemas"),
("allOf", "None for not nullable")
],
)
@pytest.mark.parametrize("is_nullable", [True, False])
def test_nullable_schema_combos(self, is_nullable, schema_type, not_nullable_regex):
"""
This test ensures that nullablilty semantics are correct for oneOf, anyOf and allOf
Specifically, nullable should checked on the children schemas
:param is_nullable: if the schema is marked as nullable. If not, validate an exception is raised on None
:param schema_type: the schema type to validate
:param not_nullable_regex: the expected raised exception if fields are marked as not nullable
"""
schema = {
"$ref": "#/$defs/Pet",
"$defs": {
"NullableText": {
"type": "string",
"nullable": False if schema_type == "oneOf" else is_nullable
},
"NullableEnum": {
"type": "string",
"nullable": is_nullable,
"enum": ["John", "Alice", None]
},
"Pet": {
"properties": {
"testfield": {
schema_type: [
{"$ref": "#/$defs/NullableText"},
{"$ref": "#/$defs/NullableEnum"},
]
}
},
}
},
}
validator = OAS30Validator(
schema,
format_checker=oas30_format_checker,
)

if is_nullable:
result = validator.validate({"testfield": None})
assert result is None
else:
with pytest.raises(
ValidationError,
match=not_nullable_regex
):
validator.validate({"testfield": None})
assert False

class TestOAS31ValidatorValidate:
@pytest.mark.parametrize(
Expand Down