Description
These keywords depend on the concept of the dynamic scope, the runtime schema traversal structure including which references were followed how often to reach the current schema+instance locations.
Note that @awwright has also filed #907 regarding this same problems space. While I don't currently see how that proposal solves the entire use case, I am very much open to being sold on other proposals for this.
Currently, $recursiveAnchor
takes a boolean which MUST be true
, and $recursiveRef
takes a URI-reference which MUST either be an empty fragment or an absolute-URI. (Note: The text says it MUST be "#"
, but we actually use absolute-URIs as well so the text is in error.).
The mechanism for how these works is described a complex process of evaluating the $recursiveRef
twice with different base URIs, depending on the presence of $recursiveAnchor
.
This is why there's a (incorrectly specified) restriction on what kind of URI-references are legal for $recursiveRef
. Path references (e.g. foo/bar
) when used with this process could produce very confusing results without any clear use case.
It's not actually that important to understand exactly how it works currently. It's probably better to think of this as a new proposal.
We at minimum need to fix the 'MUST be "#"
' problem, and it would be nice to make this less complex.
I propose changing $recursiveAnchor
so that it takes the same sort of value as $anchor
, and produces a plain name fragment identifier, just like $anchor
.
The difference between the two is that when a $recursiveAnchor
is targeted by a $recursiveRef
, the final resolved target will not necessarily be the locally declared fragment, but the fragment of the same name in the outermost dynamic scope that also declares that fragment name with $recursiveAnchor
. That's a lot of word salad, so we'll go through an example. But first:
$recursiveRef
s that do not point to a$recursiveAnchor
act just like$ref
$ref
s that point to a$recursiveAnchor
work just a if it were$anchor
- Only when
$recursiveRef
targets a$recursiveAnchor
do they behave differently.
This ensures that all schemas involved (the schema object making the reference, the local subschema with the plain name fragment, and the final resolved subschema with the plain name fragment) all intentionally opt-in to this behavior. We do not want magical behavior involving $ref
or $anchor
, ever. Most people will use $ref
and $anchor
(or at least $ref
), so they need to be simple and predictable. $recursiveRef
and $recursiveAnchor
are advanced keywords suitable for a very specific use case.
To see how this works, let's look at:
- the standard dialect schema (https://json-schema.org/draft/2019-09/schema, which describes all vocabs included in the core and validation spec)
- an abbreviated form applicator vocabulary schema (https://json-schema.org/draft/2019-09/meta/applicator), which includes only the applicator vocabulary keywords.
In all cases, I'm omitting $vocabulary
, title
, and other inessentials to save space, and of course I'm using the new $recursiveAnchor
syntax.
Applicator Vocabulary
I'm only including enough applicators here to get the point across
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/meta/applicator",
"$recursiveAnchor": "meta",
"type": ["object", "boolean"],
"properties": {
"additionalProperties": { "$recursiveRef": "#meta" },
"unevaluatedProperties": { "$recursiveRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$recursiveRef": "#meta" },
"default": {}
},
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$recursiveRef": "#meta" }
}
}
}
Standard Dialect
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/schema",
"$recursiveAnchor": "meta",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
}
When we evaluate just the applicator meta-schema against a schema, there is only one meta-schema resource involved. In this situation, $recursiveAnchor
and $recursiveRef
behave exactly like $anchor
and $ref
.
When we evaluate the standard dialect meta-schema, the $recursive*
keywords behave differently, as follows:
- We begin evaluating from the root of the standard dialect meta-schema. This is the outermost dynamic scope
- Next we follow each
allOf
branch in turn, most notably the one to"meta/applicator"
- Within the applicator meta-schema, we look at specific property schemas and eventually see a
"$recursiveRef": "#meta"
- We look for the
"#meta"
fragment declaration in the current schema resource, and find it- it is defined by"$recursiveAnchor"
, so we have more steps to take. - We look at each resource in the dynamic scope, looking for the outermost resource that also declares a
#meta
fragment with"$recursiveAnchor"
- The dialect meta-schema also has
"$recursiveAnchor": "meta"
, and is the outermost scope that does (it's also the only other scope in this example). - The resolved runtime target of the
"$recursiveRef": "#meta"
in the applicator vocabulary is the"#meta"
fragment in the dialect meta-schema.
The effect here is that since we started from the dialect schema, which declares a "$recursiveAnchor": "meta"
, and the applicator vocab schema also declares a "$recursiveAnchor": "meta"
, the "$recursiveRef": "#meta"
in the applicator vocab schema resolves to the anchor in the dialect schema. Same plain name fragment, different resource.