Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
4 changes: 2 additions & 2 deletions fastjsonschema/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
"""
Expand Down
25 changes: 19 additions & 6 deletions fastjsonschema/ref_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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)
3 changes: 0 additions & 3 deletions tests/json_schema/test_draft04.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions tests/json_schema/test_draft06.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions tests/json_schema/test_draft07.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ def pytest_generate_tests(metafunc):
'time.json',

# TODO: fix ref
'ref.json',
'id.json',
'cross-draft.json',
],
)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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