Skip to content

Commit 3d04582

Browse files
ensure that dynamic scopes are updated when following a $ref
Dynamic scopes were only being recorded while evaluating an $id keyword, but if we follow a $ref targeting a sub-resource that lacked an $id but is in a different namespace, the new scope was never recorded. See also json-schema-org/JSON-Schema-Test-Suite#794
1 parent 9f8c59b commit 3d04582

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

Changes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Revision history for JSON-Schema-Modern
33
{{$NEXT}}
44
- fix error when navigating a $dynamicRef after initial evaluation
55
at a subschema
6+
- fix incorrect $dynamicRef target following changing scopes into a
7+
subschema of a schema resource (one with an $id)
68

79
0.622 2025-11-08 22:22:19Z
810
- allow export of JSON::Schema::Modern::Utilities::is_bool

lib/JSON/Schema/Modern/Vocabulary.pm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ sub eval_subschema_at_uri ($class, $data, $schema, $state, $uri) {
8080
abort($state, 'EXCEPTION: bad reference to "%s": not a schema', $schema_info->{canonical_uri})
8181
if $schema_info->{document}->get_entity_at_location($schema_info->{document_path}) ne 'schema';
8282

83+
my $scope_uri = $schema_info->{canonical_uri}->clone->fragment(undef);
84+
push $state->{dynamic_scope}->@*, $scope_uri if $state->{dynamic_scope}->[-1] ne $scope_uri;
85+
8386
return $state->{evaluator}->_eval_subschema($data, $schema_info->{schema},
8487
+{
8588
%$state,

lib/JSON/Schema/Modern/Vocabulary/Core.pm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ sub _eval_keyword_id ($class, $data, $schema, $state) {
126126
$state->{keyword_path} = '';
127127
$state->@{qw(specification_version vocabularies)} = $schema_info->@{qw(specification_version vocabularies)};
128128

129-
push $state->{dynamic_scope}->@*, $state->{initial_schema_uri};
129+
push $state->{dynamic_scope}->@*, $state->{initial_schema_uri}
130+
if $state->{dynamic_scope}->[-1] ne $schema_info->{canonical_uri};
130131

131132
return 1;
132133
}

t/ref.t

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,72 @@ subtest '$dynamicRef to $dynamicAnchor not directly in the evaluation path' => s
10411041
);
10421042
};
10431043

1044+
subtest 'multiple layers in the dynamic scope' => sub {
1045+
$js->{_resource_index} = {};
1046+
my $schema = {
1047+
# We $ref from base -> first#/$defs/stuff -> second#/$defs/stuff -> third#/$defs/stuff
1048+
# and then follow a $dynamicRef to #length.
1049+
# At no point do we ever actually evaluate at the root schema for each scope.
1050+
# The dynamic scope is [ base, first, second, third ] and we check the scopes in order,
1051+
# therefore the first scope we find with a dynamic anchor "length" is "second".
1052+
'$id' => 'base',
1053+
'$ref' => 'first#/$defs/stuff',
1054+
'$defs' => {
1055+
first => {
1056+
'$id' => 'first',
1057+
'$defs' => {
1058+
stuff => { # first#/$defs/stuff
1059+
'$ref' => 'second#/$defs/stuff',
1060+
},
1061+
length => { # first#length
1062+
# no $dynamicAnchor here!
1063+
maxLength => 1,
1064+
},
1065+
},
1066+
},
1067+
second => {
1068+
'$id' => 'second',
1069+
'$defs' => {
1070+
stuff => { # second#/$defs/stuff
1071+
'$ref' => 'third#/$defs/stuff',
1072+
},
1073+
length => { # second#length
1074+
'$dynamicAnchor' => 'length',
1075+
maxLength => 2, # <-- this is the scope that we should find and evaluate
1076+
},
1077+
},
1078+
},
1079+
third => {
1080+
'$id' => 'third',
1081+
'$defs' => {
1082+
stuff => { # third#/$defs/stuff
1083+
'$dynamicRef' => '#length',
1084+
},
1085+
length => { # third#length
1086+
'$dynamicAnchor' => 'length',
1087+
maxLength => 3, # this should never get evaluated
1088+
}
1089+
},
1090+
},
1091+
},
1092+
};
1093+
cmp_result(
1094+
$js->evaluate('hello', $schema)->TO_JSON,
1095+
{
1096+
valid => false,
1097+
errors => [
1098+
{
1099+
instanceLocation => '',
1100+
keywordLocation => '/$ref/$ref/$ref/$dynamicRef/maxLength',
1101+
absoluteKeywordLocation => 'second#/$defs/length/maxLength',
1102+
error => 'length is greater than 2',
1103+
},
1104+
],
1105+
},
1106+
'dynamic scopes are pushed onto the stack even when its root resource (and $id keyword) are not directly evaluated',
1107+
);
1108+
};
1109+
10441110
subtest 'after leaving a dynamic scope, it should not be used by a $dynamicRef' => sub {
10451111
$js->{_resource_index} = {};
10461112
my $schema = {

0 commit comments

Comments
 (0)