From 13251e58c1851c868f8d1e32cbc2512b7f130657 Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:32:40 -0700 Subject: [PATCH 1/2] Fix nested local reference resolution --- src/check_jsonschema/schema_loader/main.py | 2 ++ tests/acceptance/test_local_relative_ref.py | 37 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/check_jsonschema/schema_loader/main.py b/src/check_jsonschema/schema_loader/main.py index ef808becd..208cfb0de 100644 --- a/src/check_jsonschema/schema_loader/main.py +++ b/src/check_jsonschema/schema_loader/main.py @@ -154,6 +154,8 @@ def _get_validator( ) -> jsonschema.protocols.Validator: retrieval_uri = self.get_schema_retrieval_uri() schema = self.get_schema() + if retrieval_uri is not None and "$id" not in schema: + schema = {"$id": retrieval_uri, **schema} schema_dialect = _dialect_of_schema(schema) # format checker (which may be None) diff --git a/tests/acceptance/test_local_relative_ref.py b/tests/acceptance/test_local_relative_ref.py index f421b69c9..487c49c36 100644 --- a/tests/acceptance/test_local_relative_ref.py +++ b/tests/acceptance/test_local_relative_ref.py @@ -29,6 +29,25 @@ CASE2_PASSING_DOCUMENT = {"test": "some data"} CASE2_FAILING_DOCUMENT = {"test": {"foo": "bar"}} +CASE3_MAIN_SCHEMA = { + "type": "object", + "properties": { + "pupils": { + "type": "array", + "items": {"$ref": "../person/person.schema.json"}, + } + }, +} +CASE3_PERSON_SCHEMA = { + "type": "object", + "properties": {"address": {"$ref": "../address/address.schema.json"}}, +} +CASE3_ADDRESS_SCHEMA = { + "type": "object", + "properties": {"zip_code": {"type": "number"}}, +} +CASE3_PASSING_DOCUMENT = {"pupils": [{"address": {"zip_code": 12345}}]} + def _prep_files(tmp_path, main_schema, other_schema_data, instance): main_schemafile = tmp_path / "main_schema.json" @@ -75,6 +94,24 @@ def test_local_ref_schema( run_line_simple(["--schemafile", schemafile, str(doc)]) +def test_nested_local_ref_schema(run_line_simple, tmp_path): + school_dir = tmp_path / "school" + person_dir = tmp_path / "person" + address_dir = tmp_path / "address" + school_dir.mkdir() + person_dir.mkdir() + address_dir.mkdir() + + main_schemafile = school_dir / "school.schema.json" + main_schemafile.write_text(json.dumps(CASE3_MAIN_SCHEMA)) + (person_dir / "person.schema.json").write_text(json.dumps(CASE3_PERSON_SCHEMA)) + (address_dir / "address.schema.json").write_text(json.dumps(CASE3_ADDRESS_SCHEMA)) + doc = school_dir / "school.example.json" + doc.write_text(json.dumps(CASE3_PASSING_DOCUMENT)) + + run_line_simple(["--schemafile", str(main_schemafile), str(doc)]) + + @pytest.mark.parametrize( "main_schema, other_schema_data, instance, expect_err", [ From a6a195de7f9311547a2f85637185ae5f307fb7f8 Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:14:29 -0700 Subject: [PATCH 2/2] Use retrieval URI as resolver base --- src/check_jsonschema/schema_loader/main.py | 9 +++++++-- tests/acceptance/test_local_relative_ref.py | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/check_jsonschema/schema_loader/main.py b/src/check_jsonschema/schema_loader/main.py index 208cfb0de..f819b85a8 100644 --- a/src/check_jsonschema/schema_loader/main.py +++ b/src/check_jsonschema/schema_loader/main.py @@ -7,6 +7,7 @@ import urllib.parse import jsonschema +from jsonschema_specifications import REGISTRY as SPECIFICATIONS from referencing import Registry from ..builtin_schemas import get_builtin_schema @@ -154,8 +155,6 @@ def _get_validator( ) -> jsonschema.protocols.Validator: retrieval_uri = self.get_schema_retrieval_uri() schema = self.get_schema() - if retrieval_uri is not None and "$id" not in schema: - schema = {"$id": retrieval_uri, **schema} schema_dialect = _dialect_of_schema(schema) # format checker (which may be None) @@ -166,6 +165,11 @@ def _get_validator( reference_registry = make_reference_registry( self._parsers, retrieval_uri, schema, self.disable_cache ) + reference_resolver = None + if retrieval_uri is not None and not isinstance(schema.get("$id"), str): + reference_resolver = SPECIFICATIONS.combine(reference_registry).resolver( + retrieval_uri + ) if self.validator_class is None: # get the correct validator class and check the schema under its metaschema @@ -197,6 +201,7 @@ def _get_validator( validator = validator_cls( # type: ignore[call-arg] schema, registry=reference_registry, + _resolver=reference_resolver, format_checker=format_checker, ) return t.cast(jsonschema.protocols.Validator, validator) diff --git a/tests/acceptance/test_local_relative_ref.py b/tests/acceptance/test_local_relative_ref.py index 487c49c36..fd7dc340d 100644 --- a/tests/acceptance/test_local_relative_ref.py +++ b/tests/acceptance/test_local_relative_ref.py @@ -47,6 +47,7 @@ "properties": {"zip_code": {"type": "number"}}, } CASE3_PASSING_DOCUMENT = {"pupils": [{"address": {"zip_code": 12345}}]} +CASE3_FAILING_DOCUMENT = {"pupils": [{"address": {"zip_code": "invalid"}}]} def _prep_files(tmp_path, main_schema, other_schema_data, instance): @@ -94,7 +95,14 @@ def test_local_ref_schema( run_line_simple(["--schemafile", schemafile, str(doc)]) -def test_nested_local_ref_schema(run_line_simple, tmp_path): +@pytest.mark.parametrize( + "instance, expect_err", + [ + (CASE3_PASSING_DOCUMENT, None), + (CASE3_FAILING_DOCUMENT, "'invalid' is not of type 'number'"), + ], +) +def test_nested_local_ref_schema(run_line, tmp_path, instance, expect_err): school_dir = tmp_path / "school" person_dir = tmp_path / "person" address_dir = tmp_path / "address" @@ -107,9 +115,12 @@ def test_nested_local_ref_schema(run_line_simple, tmp_path): (person_dir / "person.schema.json").write_text(json.dumps(CASE3_PERSON_SCHEMA)) (address_dir / "address.schema.json").write_text(json.dumps(CASE3_ADDRESS_SCHEMA)) doc = school_dir / "school.example.json" - doc.write_text(json.dumps(CASE3_PASSING_DOCUMENT)) + doc.write_text(json.dumps(instance)) - run_line_simple(["--schemafile", str(main_schemafile), str(doc)]) + res = run_line(["check-jsonschema", "--schemafile", str(main_schemafile), str(doc)]) + assert res.exit_code == (1 if expect_err is not None else 0) + if expect_err is not None: + assert expect_err in res.stdout @pytest.mark.parametrize(