Skip to content

Commit 5ce7d30

Browse files
authored
Merge pull request #57 from pyckle/master
Fix #20 - nullable semantics with $ref, oneOf, anyOf, and allOf
2 parents 553d606 + 5b8e47b commit 5ce7d30

File tree

3 files changed

+132
-33
lines changed

3 files changed

+132
-33
lines changed

openapi_schema_validator/_validators.py

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,6 @@
1818
from jsonschema.protocols import Validator
1919

2020

21-
def include_nullable_validator(
22-
schema: Dict[Hashable, Any]
23-
) -> ItemsView[Hashable, Any]:
24-
"""
25-
Include ``nullable`` validator always.
26-
Suitable for use with `create`'s ``applicable_validators`` argument.
27-
"""
28-
_schema = deepcopy(schema)
29-
30-
# append defaults to trigger nullable validator
31-
if "nullable" not in _schema:
32-
_schema.update(
33-
{
34-
"nullable": False,
35-
}
36-
)
37-
38-
return _schema.items()
39-
40-
4121
def handle_discriminator(
4222
validator: Validator, _: Any, instance: Any, schema: Mapping[Hashable, Any]
4323
) -> Iterator[ValidationError]:
@@ -127,7 +107,14 @@ def type(
127107
schema: Mapping[Hashable, Any],
128108
) -> Iterator[ValidationError]:
129109
if instance is None:
130-
return
110+
# nullable implementation based on OAS 3.0.3
111+
# * nullable is only meaningful if its value is true
112+
# * nullable: true is only meaningful in combination with a type
113+
# assertion specified in the same Schema Object.
114+
# * nullable: true operates within a single Schema Object
115+
if "nullable" in schema and schema["nullable"] == True:
116+
return
117+
yield ValidationError("None for not nullable")
131118

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

165152

166-
def nullable(
167-
validator: Validator,
168-
is_nullable: bool,
169-
instance: Any,
170-
schema: Mapping[Hashable, Any],
171-
) -> Iterator[ValidationError]:
172-
if instance is None and not is_nullable:
173-
yield ValidationError("None for not nullable")
174-
175-
176153
def required(
177154
validator: Validator,
178155
required: List[str],

openapi_schema_validator/validators.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
# TODO: adjust default
4646
"$ref": _validators.ref,
4747
# fixed OAS fields
48-
"nullable": oas_validators.nullable,
4948
"discriminator": oas_validators.not_implemented,
5049
"readOnly": oas_validators.readOnly,
5150
"writeOnly": oas_validators.writeOnly,
@@ -59,7 +58,6 @@
5958
# See https://github.com/p1c2u/openapi-schema-validator/pull/12
6059
# version="oas30",
6160
id_of=lambda schema: schema.get("id", ""),
62-
applicable_validators=oas_validators.include_nullable_validator,
6361
)
6462

6563
OAS31Validator = extend(

tests/integration/test_validators.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ def test_null(self, schema_type):
3131
with pytest.raises(ValidationError):
3232
validator.validate(value)
3333

34+
@pytest.mark.parametrize("is_nullable", [True, False])
35+
def test_nullable_untyped(self, is_nullable):
36+
schema = {"nullable": is_nullable}
37+
validator = OAS30Validator(schema)
38+
value = None
39+
40+
result = validator.validate(value)
41+
42+
assert result is None
43+
3444
@pytest.mark.parametrize(
3545
"schema_type",
3646
[
@@ -50,6 +60,23 @@ def test_nullable(self, schema_type):
5060

5161
assert result is None
5262

63+
def test_nullable_enum_without_none(self):
64+
schema = {"type": "integer", "nullable": True, "enum": [1, 2, 3]}
65+
validator = OAS30Validator(schema)
66+
value = None
67+
68+
with pytest.raises(ValidationError):
69+
validator.validate(value)
70+
71+
def test_nullable_enum_with_none(self):
72+
schema = {"type": "integer", "nullable": True, "enum": [1, 2, 3, None]}
73+
validator = OAS30Validator(schema)
74+
value = None
75+
76+
result = validator.validate(value)
77+
78+
assert result is None
79+
5380
@pytest.mark.parametrize(
5481
"value",
5582
[
@@ -442,6 +469,103 @@ def test_oneof_discriminator(self, schema_type):
442469
result = validator.validate({"discipline": "other"})
443470
assert False
444471

472+
@pytest.mark.parametrize("is_nullable", [True, False])
473+
def test_nullable_ref(self, is_nullable):
474+
"""
475+
Tests that a field that points to a schema reference is null checked based on the $ref schema rather than
476+
on this schema
477+
:param is_nullable: if the schema is marked as nullable. If not, validate an exception is raised on None
478+
"""
479+
schema = {
480+
"$ref": "#/$defs/Pet",
481+
"$defs": {
482+
"NullableText": {
483+
"type": "string",
484+
"nullable": is_nullable
485+
},
486+
"Pet": {
487+
"properties": {
488+
"testfield": {"$ref": "#/$defs/NullableText"},
489+
},
490+
}
491+
},
492+
}
493+
validator = OAS30Validator(
494+
schema,
495+
format_checker=oas30_format_checker,
496+
)
497+
498+
result = validator.validate({"testfield": "John"})
499+
assert result is None
500+
501+
if is_nullable:
502+
result = validator.validate({"testfield": None})
503+
assert result is None
504+
else:
505+
with pytest.raises(
506+
ValidationError,
507+
match="None for not nullable",
508+
):
509+
validator.validate({"testfield": None})
510+
assert False
511+
512+
513+
@pytest.mark.parametrize(
514+
"schema_type, not_nullable_regex",
515+
[
516+
("oneOf", "None is not valid under any of the given schemas"),
517+
("anyOf", "None is not valid under any of the given schemas"),
518+
("allOf", "None for not nullable")
519+
],
520+
)
521+
@pytest.mark.parametrize("is_nullable", [True, False])
522+
def test_nullable_schema_combos(self, is_nullable, schema_type, not_nullable_regex):
523+
"""
524+
This test ensures that nullablilty semantics are correct for oneOf, anyOf and allOf
525+
Specifically, nullable should checked on the children schemas
526+
:param is_nullable: if the schema is marked as nullable. If not, validate an exception is raised on None
527+
:param schema_type: the schema type to validate
528+
:param not_nullable_regex: the expected raised exception if fields are marked as not nullable
529+
"""
530+
schema = {
531+
"$ref": "#/$defs/Pet",
532+
"$defs": {
533+
"NullableText": {
534+
"type": "string",
535+
"nullable": False if schema_type == "oneOf" else is_nullable
536+
},
537+
"NullableEnum": {
538+
"type": "string",
539+
"nullable": is_nullable,
540+
"enum": ["John", "Alice", None]
541+
},
542+
"Pet": {
543+
"properties": {
544+
"testfield": {
545+
schema_type: [
546+
{"$ref": "#/$defs/NullableText"},
547+
{"$ref": "#/$defs/NullableEnum"},
548+
]
549+
}
550+
},
551+
}
552+
},
553+
}
554+
validator = OAS30Validator(
555+
schema,
556+
format_checker=oas30_format_checker,
557+
)
558+
559+
if is_nullable:
560+
result = validator.validate({"testfield": None})
561+
assert result is None
562+
else:
563+
with pytest.raises(
564+
ValidationError,
565+
match=not_nullable_regex
566+
):
567+
validator.validate({"testfield": None})
568+
assert False
445569

446570
class TestOAS31ValidatorValidate:
447571
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)