From e213079d9060a149f3aa783a7a470ed73844b05a Mon Sep 17 00:00:00 2001 From: Andrew Pikler Date: Thu, 5 Jan 2023 10:36:33 +0200 Subject: [PATCH 1/2] Fix #20 - nullable semantics with $ref, oneOf, anyOf, and allOf --- openapi_schema_validator/_validators.py | 8 +- tests/integration/test_validators.py | 97 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/openapi_schema_validator/_validators.py b/openapi_schema_validator/_validators.py index b48f694..6aa4d9e 100644 --- a/openapi_schema_validator/_validators.py +++ b/openapi_schema_validator/_validators.py @@ -27,8 +27,12 @@ def include_nullable_validator( """ _schema = deepcopy(schema) - # append defaults to trigger nullable validator - if "nullable" not in _schema: + # append defaults to trigger nullable validator, except where $ref in the schema checks null + if "nullable" not in _schema \ + and "$ref" not in _schema \ + and "oneOf" not in _schema \ + and "anyOf" not in _schema \ + and "allOf" not in _schema: _schema.update( { "nullable": False, diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 47ea5da..8817829 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -442,6 +442,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( From 5b8e47bdad4269670a6b7607d4b062d91d630259 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Sat, 7 Jan 2023 03:20:30 +0000 Subject: [PATCH 2/2] nullable implementation based on clarified OAS 3.0.3 definition --- openapi_schema_validator/_validators.py | 43 +++++-------------------- openapi_schema_validator/validators.py | 2 -- tests/integration/test_validators.py | 27 ++++++++++++++++ 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/openapi_schema_validator/_validators.py b/openapi_schema_validator/_validators.py index 6aa4d9e..f0144ab 100644 --- a/openapi_schema_validator/_validators.py +++ b/openapi_schema_validator/_validators.py @@ -18,30 +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, except where $ref in the schema checks null - if "nullable" not in _schema \ - and "$ref" not in _schema \ - and "oneOf" not in _schema \ - and "anyOf" not in _schema \ - and "allOf" not in _schema: - _schema.update( - { - "nullable": False, - } - ) - - return _schema.items() - - def handle_discriminator( validator: Validator, _: Any, instance: Any, schema: Mapping[Hashable, Any] ) -> Iterator[ValidationError]: @@ -131,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) @@ -167,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 8817829..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", [ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy