diff --git a/openapi_spec_validator/schemas/utils.py b/openapi_spec_validator/schemas/utils.py index d3d2fba..30c99a1 100644 --- a/openapi_spec_validator/schemas/utils.py +++ b/openapi_spec_validator/schemas/utils.py @@ -7,9 +7,12 @@ from typing import Tuple if sys.version_info >= (3, 9): - from importlib.resources import as_file, files + from importlib.resources import as_file + from importlib.resources import files else: - from importlib_resources import as_file, files + from importlib_resources import as_file + from importlib_resources import files + from jsonschema_spec.readers import FilePathReader diff --git a/openapi_spec_validator/validation/validators.py b/openapi_spec_validator/validation/validators.py index db4b3d7..2e2d892 100644 --- a/openapi_spec_validator/validation/validators.py +++ b/openapi_spec_validator/validation/validators.py @@ -65,6 +65,7 @@ def __init__( self.resolver_handlers = resolver_handlers self.operation_ids_registry: Optional[List[str]] = None + self.schema_ids_registry: Optional[List[int]] = None self.resolver = None def validate( @@ -82,6 +83,7 @@ def iter_errors( self, instance: Mapping[Hashable, Any], spec_url: str = "" ) -> Iterator[ValidationError]: self.operation_ids_registry = [] + self.schema_ids_registry = [] self.resolver = self._get_resolver(spec_url, instance) yield from self.schema_validator.iter_errors(instance) @@ -248,7 +250,12 @@ def _iter_schema_errors( if not hasattr(schema.content(), "__getitem__"): return - schema_type = schema.getkey("type") + assert self.schema_ids_registry is not None + schema_id = id(schema.content()) + if schema_id in self.schema_ids_registry: + return + self.schema_ids_registry.append(schema_id) + nested_properties = [] if "allOf" in schema: all_of = schema / "allOf" @@ -294,7 +301,7 @@ def _iter_schema_errors( ) if "properties" in schema: - props = schema /"properties" + props = schema / "properties" for _, prop_schema in props.items(): yield from self._iter_schema_errors( prop_schema, diff --git a/tests/integration/data/v3.0/property-recursive.yaml b/tests/integration/data/v3.0/property-recursive.yaml new file mode 100644 index 0000000..c1d704b --- /dev/null +++ b/tests/integration/data/v3.0/property-recursive.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + version: '1.0.0' + title: 'Some Schema' +paths: {} +components: + schemas: + SomeDataType: + type: object + properties: + id: + type: integer + prop1: + $ref: '#/components/schemas/SomeDataType' + prop2: + type: string \ No newline at end of file diff --git a/tests/integration/validation/test_exceptions.py b/tests/integration/validation/test_exceptions.py index cae8140..129e0f1 100644 --- a/tests/integration/validation/test_exceptions.py +++ b/tests/integration/validation/test_exceptions.py @@ -456,7 +456,9 @@ def test_parameter_custom_format_checker_not_found(self, validator_v30): errors_list = list(errors) assert errors_list == [] - def test_parameter_default_value_custom_format_invalid(self, validator_v30): + def test_parameter_default_value_custom_format_invalid( + self, validator_v30 + ): from openapi_schema_validator import oas30_format_checker @oas30_format_checker.checks("custom") @@ -498,6 +500,4 @@ def validate(to_validate) -> bool: errors_list = list(errors) assert len(errors_list) == 1 assert errors_list[0].__class__ == OpenAPIValidationError - assert errors_list[0].message == ( - "'invalid' is not a 'custom'" - ) + assert errors_list[0].message == ("'invalid' is not a 'custom'") diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index 0112777..680d883 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -66,6 +66,7 @@ def local_test_suite_file_path(self, test_file): "petstore.yaml", "petstore-separate/spec/openapi.yaml", "parent-reference/openapi.yaml", + "property-recursive.yaml", ], ) def test_valid(self, factory, validator_v30, spec_file):