diff --git a/openapi_schema_validator/_validators.py b/openapi_schema_validator/_validators.py index b48f694..f0144ab 100644 --- a/openapi_schema_validator/_validators.py +++ b/openapi_schema_validator/_validators.py @@ -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]: @@ -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) @@ -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], diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index c8d68b4..f4b12ea 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -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, @@ -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( diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 47ea5da..1bbc9f6 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -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", [ @@ -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", [ @@ -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(