Skip to content

Improve/simplify "$recursiveAnchor" and "$recursiveRef" #909

Closed
@handrews

Description

@handrews

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:

  • $recursiveRefs that do not point to a $recursiveAnchor act just like $ref
  • $refs 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:

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:

  1. We begin evaluating from the root of the standard dialect meta-schema. This is the outermost dynamic scope
  2. Next we follow each allOf branch in turn, most notably the one to "meta/applicator"
  3. Within the applicator meta-schema, we look at specific property schemas and eventually see a "$recursiveRef": "#meta"
  4. 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.
  5. We look at each resource in the dynamic scope, looking for the outermost resource that also declares a #meta fragment with "$recursiveAnchor"
  6. 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).
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions