Skip to content

Commit c1d69d9

Browse files
committed
fix: recheck $recursiveAnchor presence for $recursiveRef targets
We treat it a mistake to not have a $recursiveAnchor: true in the schema that uses $recursiveRef, so that throws in non-lax modes. Refs: json-schema-org/JSON-Schema-Test-Suite#391
1 parent 5b53fc7 commit c1d69d9

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-0
lines changed

src/compile.js

+9
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,15 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
951951
}
952952
handle('$recursiveRef', ['string'], ($recursiveRef) => {
953953
enforce($recursiveRef === '#', 'Behavior of $recursiveRef is defined only for "#"')
954+
// Resolve to recheck that recursive ref is enabled
955+
const resolved = resolveReference(root, schemas, '#', basePath())
956+
const [sub, subRoot, path] = resolved[0] || []
957+
laxMode(sub.$recursiveAnchor, '$recursiveRef without $recursiveAnchor')
958+
if (!sub.$recursiveAnchor || !recursiveAnchor) {
959+
// regular ref
960+
const n = getref(sub) || compileSchema(sub, subRoot, opts, scope, path)
961+
return applyRef(n, { path: ['$recursiveRef'] })
962+
}
954963
// Apply deep recursion from here only if $recursiveAnchor is true, else just run self
955964
const n = recursiveAnchor ? format('(recursive || validate)') : format('validate')
956965
return applyRef(n, { path: ['$recursiveRef'] })
+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
[
2+
{
3+
"description": "$recursiveRef without $recursiveAnchor works like $ref",
4+
"schema": {
5+
"properties": {
6+
"foo": { "$recursiveRef": "#" }
7+
},
8+
"additionalProperties": false
9+
},
10+
"tests": [
11+
{
12+
"description": "match",
13+
"data": {"foo": false},
14+
"valid": true
15+
},
16+
{
17+
"description": "recursive match",
18+
"data": { "foo": { "foo": false } },
19+
"valid": true
20+
},
21+
{
22+
"description": "mismatch",
23+
"data": { "bar": false },
24+
"valid": false
25+
},
26+
{
27+
"description": "recursive mismatch",
28+
"data": { "foo": { "bar": false } },
29+
"valid": false
30+
}
31+
]
32+
},
33+
{
34+
"description": "$recursiveRef without using nesting",
35+
"schema": {
36+
"$id": "http://localhost:4242/recursiveRef2/schema.json",
37+
"$defs": {
38+
"myobject": {
39+
"$id": "myobject.json",
40+
"$recursiveAnchor": true,
41+
"anyOf": [
42+
{ "type": "string" },
43+
{
44+
"type": "object",
45+
"additionalProperties": { "$recursiveRef": "#" }
46+
}
47+
]
48+
}
49+
},
50+
"anyOf": [
51+
{ "type": "integer" },
52+
{ "$ref": "#/$defs/myobject" }
53+
]
54+
},
55+
"tests": [
56+
{
57+
"description": "integer matches at the outer level",
58+
"data": 1,
59+
"valid": true
60+
},
61+
{
62+
"description": "single level match",
63+
"data": { "foo": "hi" },
64+
"valid": true
65+
},
66+
{
67+
"description": "integer does not match as a property value",
68+
"data": { "foo": 1 },
69+
"valid": false
70+
},
71+
{
72+
"description": "two levels, properties match with inner definition",
73+
"data": { "foo": { "bar": "hi" } },
74+
"valid": true
75+
},
76+
{
77+
"description": "two levels, no match",
78+
"data": { "foo": { "bar": 1 } },
79+
"valid": false
80+
}
81+
]
82+
},
83+
{
84+
"description": "$recursiveRef with nesting",
85+
"schema": {
86+
"$id": "http://localhost:4242/recursiveRef3/schema.json",
87+
"$recursiveAnchor": true,
88+
"$defs": {
89+
"myobject": {
90+
"$id": "myobject.json",
91+
"$recursiveAnchor": true,
92+
"anyOf": [
93+
{ "type": "string" },
94+
{
95+
"type": "object",
96+
"additionalProperties": { "$recursiveRef": "#" }
97+
}
98+
]
99+
}
100+
},
101+
"anyOf": [
102+
{ "type": "integer" },
103+
{ "$ref": "#/$defs/myobject" }
104+
]
105+
},
106+
"tests": [
107+
{
108+
"description": "integer matches at the outer level",
109+
"data": 1,
110+
"valid": true
111+
},
112+
{
113+
"description": "single level match",
114+
"data": { "foo": "hi" },
115+
"valid": true
116+
},
117+
{
118+
"description": "integer now matches as a property value",
119+
"data": { "foo": 1 },
120+
"valid": true
121+
},
122+
{
123+
"description": "two levels, properties match with inner definition",
124+
"data": { "foo": { "bar": "hi" } },
125+
"valid": true
126+
},
127+
{
128+
"description": "two levels, properties match with $recursiveRef",
129+
"data": { "foo": { "bar": 1 } },
130+
"valid": true
131+
}
132+
]
133+
},
134+
{
135+
"description": "$recursiveRef with $recursiveAnchor: false works like $ref",
136+
"schema": {
137+
"$id": "http://localhost:4242/recursiveRef4/schema.json",
138+
"$recursiveAnchor": false,
139+
"$defs": {
140+
"myobject": {
141+
"$id": "myobject.json",
142+
"$recursiveAnchor": false,
143+
"anyOf": [
144+
{ "type": "string" },
145+
{
146+
"type": "object",
147+
"additionalProperties": { "$recursiveRef": "#" }
148+
}
149+
]
150+
}
151+
},
152+
"anyOf": [
153+
{ "type": "integer" },
154+
{ "$ref": "#/$defs/myobject" }
155+
]
156+
},
157+
"tests": [
158+
{
159+
"description": "integer matches at the outer level",
160+
"data": 1,
161+
"valid": true
162+
},
163+
{
164+
"description": "single level match",
165+
"data": { "foo": "hi" },
166+
"valid": true
167+
},
168+
{
169+
"description": "integer does not match as a property value",
170+
"data": { "foo": 1 },
171+
"valid": false
172+
},
173+
{
174+
"description": "two levels, properties match with inner definition",
175+
"data": { "foo": { "bar": "hi" } },
176+
"valid": true
177+
},
178+
{
179+
"description": "two levels, integer does not match as a property value",
180+
"data": { "foo": { "bar": 1 } },
181+
"valid": false
182+
}
183+
]
184+
},
185+
{
186+
"description": "$recursiveRef with no $recursiveAnchor works like $ref",
187+
"schema": {
188+
"$id": "http://localhost:4242/recursiveRef5/schema.json",
189+
"$defs": {
190+
"myobject": {
191+
"$id": "myobject.json",
192+
"$recursiveAnchor": false,
193+
"anyOf": [
194+
{ "type": "string" },
195+
{
196+
"type": "object",
197+
"additionalProperties": { "$recursiveRef": "#" }
198+
}
199+
]
200+
}
201+
},
202+
"anyOf": [
203+
{ "type": "integer" },
204+
{ "$ref": "#/$defs/myobject" }
205+
]
206+
},
207+
"tests": [
208+
{
209+
"description": "integer matches at the outer level",
210+
"data": 1,
211+
"valid": true
212+
},
213+
{
214+
"description": "single level match",
215+
"data": { "foo": "hi" },
216+
"valid": true
217+
},
218+
{
219+
"description": "integer does not match as a property value",
220+
"data": { "foo": 1 },
221+
"valid": false
222+
},
223+
{
224+
"description": "two levels, properties match with inner definition",
225+
"data": { "foo": { "bar": "hi" } },
226+
"valid": true
227+
},
228+
{
229+
"description": "two levels, integer does not match as a property value",
230+
"data": { "foo": { "bar": 1 } },
231+
"valid": false
232+
}
233+
]
234+
},
235+
{
236+
"description": "$recursiveRef with no $recursiveAnchor in the initial target schema resource",
237+
"schema": {
238+
"$id": "http://localhost:4242/recursiveRef6/base.json",
239+
"$recursiveAnchor": true,
240+
"anyOf": [
241+
{ "type": "boolean" },
242+
{
243+
"type": "object",
244+
"additionalProperties": {
245+
"$id": "http://localhost:4242/recursiveRef6/inner.json",
246+
"$comment": "there is no $recursiveAnchor: true here, so we do NOT recurse to the base",
247+
"anyOf": [
248+
{ "type": "integer" },
249+
{ "type": "object", "additionalProperties": { "$recursiveRef": "#" } }
250+
]
251+
}
252+
}
253+
]
254+
},
255+
"tests": [
256+
{
257+
"description": "leaf node does not match; no recursion",
258+
"data": { "foo": true },
259+
"valid": false
260+
},
261+
{
262+
"description": "leaf node matches: recursion only uses inner schema",
263+
"data": { "foo": { "bar": 1 } },
264+
"valid": true
265+
},
266+
{
267+
"description": "leaf node does not match: recursion only uses inner schema",
268+
"data": { "foo": { "bar": true } },
269+
"valid": false
270+
}
271+
]
272+
}
273+
]

test/json-schema.js

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const unsafe = new Set([
3030

3131
'ref.json/ref overrides any sibling keywords', // this was fixed in draft/2019-09 spec
3232

33+
// tests $recursiveRef without $recursiveAnchor, we treat this as a mistake
34+
'extra-tests/recursiveRef.391.json/$recursiveRef without $recursiveAnchor works like $ref',
35+
'extra-tests/recursiveRef.391.json/$recursiveRef with $recursiveAnchor: false works like $ref',
36+
'extra-tests/recursiveRef.391.json/$recursiveRef with no $recursiveAnchor works like $ref',
37+
'extra-tests/recursiveRef.391.json/$recursiveRef with no $recursiveAnchor in the initial target schema resource',
38+
3339
// draft3 only
3440
'draft3/additionalItems.json/additionalItems should not look in applicators',
3541
'draft3/additionalProperties.json/additionalProperties should not look in applicators',

0 commit comments

Comments
 (0)