From ec0768891dbafbbcee957761cd810213df81de1e Mon Sep 17 00:00:00 2001 From: Chinmay Jha Date: Sat, 7 Nov 2026 00:34:45 +0530 Subject: [PATCH] Resolve $id and $ref inside schema arrays RefResolver.walk only descended into dict values, so subschemas in allOf, anyOf, oneOf and items arrays were never walked: an $id there was not registered, and a relative $ref there was not joined with the base URI of its parent $id. It also walked into const and default values, rewriting a $ref there and registering an $id from data. Walk the keywords that hold subschemas and skip the data keywords. ref.json passes for draft 06 and 07, and id.json for draft 04, 06 and 07. --- CHANGELOG.txt | 2 ++ fastjsonschema/generator.py | 4 ++-- fastjsonschema/ref_resolver.py | 25 +++++++++++++++++++------ tests/json_schema/test_draft04.py | 3 --- tests/json_schema/test_draft06.py | 4 ---- tests/json_schema/test_draft07.py | 2 -- tests/test_integration.py | 19 +++++++++++++++++++ 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dd8f8fd..0362273 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ === UNRELEASED * Fixed `$` in `patternProperties` matching before a trailing newline, which also made a `pattern` with the same regex accept a trailing newline +* Fixed `$ref` resolution for subschemas inside `allOf`, `anyOf`, `oneOf` and `items` arrays: an `$id` (or draft 04 `id`) there was not found, and a relative `$ref` there ignored the base URI of its parent `$id` +* Fixed `$id` and `$ref` inside `const`, `default`, `enum` and `examples` values being treated as schema keywords (a `$ref` there was rewritten to an absolute URI) === 2.22.3 (2026-11-01) diff --git a/fastjsonschema/generator.py b/fastjsonschema/generator.py index 951edda..e3f01b0 100644 --- a/fastjsonschema/generator.py +++ b/fastjsonschema/generator.py @@ -5,7 +5,7 @@ from .exceptions import JsonSchemaValueException, JsonSchemaValuesException, JsonSchemaDefinitionException from .indent import indent -from .ref_resolver import RefResolver +from .ref_resolver import DATA_KEYWORDS, RefResolver # Both mean "this subschema did not match": a subschema behind a $ref becomes its own # function, which reports through JsonSchemaValuesException while errors are collected. @@ -331,7 +331,7 @@ def _expand_refs(self, definition): if "$ref" in definition and isinstance(definition["$ref"], str): with self._resolver.resolving(definition["$ref"]) as schema: return schema - return {k: self._expand_refs(v) for k, v in definition.items()} + return {k: v if k in DATA_KEYWORDS else self._expand_refs(v) for k, v in definition.items()} def _is_variable_in_scope(self, variable_name): """ diff --git a/fastjsonschema/ref_resolver.py b/fastjsonschema/ref_resolver.py index 67fcbdd..9797778 100644 --- a/fastjsonschema/ref_resolver.py +++ b/fastjsonschema/ref_resolver.py @@ -20,6 +20,9 @@ from .exceptions import JsonSchemaDefinitionException MAX_SCHEMA_WALK_DEPTH = min(500, sys.getrecursionlimit() // 2) +SUBSCHEMA_ARRAY_KEYWORDS = ('allOf', 'anyOf', 'oneOf', 'items') +SUBSCHEMA_MAP_KEYWORDS = ('properties', 'patternProperties', 'definitions', '$defs', 'dependencies', 'dependentSchemas') +DATA_KEYWORDS = ('const', 'default', 'enum', 'examples') def get_id(schema: dict[str, Any]) -> str: @@ -29,6 +32,18 @@ def get_id(schema: dict[str, Any]) -> str: return schema.get('$id', schema.get('id', '')) +def _subschemas(node: dict[str, Any]) -> Iterator[dict[str, Any]]: + for key, item in node.items(): + if key in DATA_KEYWORDS: + continue + if key in SUBSCHEMA_MAP_KEYWORDS and isinstance(item, dict): + yield from (sub for sub in item.values() if isinstance(sub, dict)) + elif isinstance(item, dict): + yield item + elif isinstance(item, list) and key in SUBSCHEMA_ARRAY_KEYWORDS: + yield from (sub for sub in item if isinstance(sub, dict)) + + def resolve_path(schema: Any, fragment: str) -> Any: """ Return definition from path. @@ -210,10 +225,8 @@ def walk(self, node: dict[str, Any] | bool, depth: int = 0, rewrite_refs: bool = elif ('$id' in node or 'id' in node) and isinstance(get_id(node), str): with self.in_scope(get_id(node)): self.store[normalize(self.resolution_scope)] = node - for _, item in node.items(): - if isinstance(item, dict): - self.walk(item, depth + 1, rewrite_refs) - else: - for _, item in node.items(): - if isinstance(item, dict): + for item in _subschemas(node): self.walk(item, depth + 1, rewrite_refs) + else: + for item in _subschemas(node): + self.walk(item, depth + 1, rewrite_refs) diff --git a/tests/json_schema/test_draft04.py b/tests/json_schema/test_draft04.py index 6e82b26..2828d59 100644 --- a/tests/json_schema/test_draft04.py +++ b/tests/json_schema/test_draft04.py @@ -12,9 +12,6 @@ def pytest_generate_tests(metafunc): 'ecmascript-regex.json', 'float-overflow.json', 'date-time.json', - - # TODO: fix ref - 'id.json', ], ) metafunc.parametrize(['schema_version', 'schema', 'data', 'is_valid'], param_values, ids=param_ids) diff --git a/tests/json_schema/test_draft06.py b/tests/json_schema/test_draft06.py index 4eda44a..f399aee 100644 --- a/tests/json_schema/test_draft06.py +++ b/tests/json_schema/test_draft06.py @@ -13,10 +13,6 @@ def pytest_generate_tests(metafunc): 'float-overflow.json', 'unknownKeyword.json', 'date-time.json', - - # TODO: fix ref - 'ref.json', - 'id.json', ], ) metafunc.parametrize(['schema_version', 'schema', 'data', 'is_valid'], param_values, ids=param_ids) diff --git a/tests/json_schema/test_draft07.py b/tests/json_schema/test_draft07.py index 32e74f1..a2a3aad 100644 --- a/tests/json_schema/test_draft07.py +++ b/tests/json_schema/test_draft07.py @@ -21,8 +21,6 @@ def pytest_generate_tests(metafunc): 'time.json', # TODO: fix ref - 'ref.json', - 'id.json', 'cross-draft.json', ], ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1ee578b..fc1d3f1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -167,3 +167,22 @@ def test_relative_ref_in_remote_schema_is_resolved_once(): ) assert validator({'value': 'ok'}) == {'value': 'ok'} + + + +def test_ref_inside_const_is_data(): + schema = { + '$id': 'http://example.com/root.json', + 'properties': { + 'a': { + '$id': 'sub/schema.json', + 'definitions': {'string': {'type': 'string'}}, + 'const': {'$ref': '#/definitions/string'}, + }, + }, + } + + validator = compile(schema) + + value = {'a': {'$ref': '#/definitions/string'}} + assert validator(value) == value