Skip to content

Commit

Permalink
Merge pull request #57 from pyckle/master
Browse files Browse the repository at this point in the history
Fix #20 - nullable semantics with $ref, oneOf, anyOf, and allOf
  • Loading branch information
p1c2u committed Jan 7, 2023
2 parents 553d606 + 5b8e47b commit 5ce7d30
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 33 deletions.
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

0 comments on commit 5ce7d30

Please sign in to comment.