Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Remove initial resolution step for dynamic references #1140

Open
jdesrosiers opened this issue Oct 20, 2021 · 17 comments
Open

Proposal: Remove initial resolution step for dynamic references #1140

jdesrosiers opened this issue Oct 20, 2021 · 17 comments
Assignees

Comments

@jdesrosiers
Copy link
Member

jdesrosiers commented Oct 20, 2021

This issue is a spin-off of #1064. That conversation drifted a bit, so I'm moving some of the proposed amendments to that proposal into a separate issue.

Removing the bookending requirement for dynamic references opens the door for some additional considerations. $recrusiveRef didn't allow non-fragment URI parts. $dynamicRef added that in order to allow using $dynamicRef in a non-recursive schema. However, without the bookending requirement, the initial resolution step is no longer necessary (see #1064 (comment)). Since it's not necessary, there exists the possibility that a $dynamicRef doesn't need to be a URI at all. It makes sense for it to just be the dynamic anchor name. There's no reason to encode it into a URI. (Credit to @ssilverman for this idea) (See also #1064 (comment)).

We could take this a step further and make dynamic anchors URIs instead of plain name identifiers. Because dynamic anchors aren't scoped to the local document like traditional anchors, there's a possibility of name conflicts between schemas, so using URI identifiers would be safer. It's a similar concept to using URIs for link relations (for those who are familiar with those).

@jdesrosiers
Copy link
Member Author

Here's an example that sets a dynamic anchor with the identifier https://json-schema.hyperjump.io/anchor/list and dynamically references it.

{
  "$id": "https://json-schema.hyperjump.io/schema/list",
  "$schema": "https://json-schema.org/draft/2020-12/schema",

  "type": "object",
  "properties": {
    "list": {
      "type": "array",
      "items": { "$dynamicRef": "/anchor/llist" }
    },
    "nextPage": { "type": "integer" },
    "previousPage": { "type": "integer" },
    "perPage": { "type": "integer" },
    "page": { "type": "integer" }
  }
}
{
  "$id": "https://json-schema.hyperjump.io/schema/foo-list",
  "$schema": "https://json-schema.org/draft/2020-12/schema",

  "$ref": "/schema/list",

  "$defs": {
    "foo": {
      "$dynamicAnchor": "/anchor/list",
      "$ref": "/schema/foo"
    }
  }
}

@jdesrosiers
Copy link
Member Author

I couldn't help writing up what the spec might look like without bookending or initial resolution (#1142). It makes the concept much easier to describe. Personally, I think it's easier to understand as well.

@jdesrosiers
Copy link
Member Author

Here is an example using the hyper-schema links schema that shows what is lost by removing the initial resolution step. In this case we want "submissionSchema" to be extendible with a dynamic reference, but we also want it to default to a specific draft. With initial resolution, we can do this.

{
  "$id": "https://json-schema.org/draft/2020-12/links",
  "properties": {
    "submissionSchema": { "$dynamicRef": "/draft/2020-12/hyper-schema#meta" }
  }
}

The dynamic reference first resolves to the 2020-12 dialect where a "meta" dynamic anchor enters the dynamic scope. Since there are no other "meta" dynamic anchors already in this scope, the 2020-12 dialect is used. Without the initial resolution, we can get the same behavior, but it's more verbose.

{
  "$id": "https://json-schema.org/draft/2020-12/links",
  "properties": {
    "submissionSchema": { "$dynamicRef": "meta" }
  },
  "$defs": {
    "defaultDialect": {
      "$dynamicAnchor": "meta",
      "$ref": "/draft/2020-12/hyper-schema"
    }
  }
}

In order for there to be a default place for the dynamic reference to resolve, we have to explicitly set a dynamic anchor somewhere in the schema to define the default.

@handrews
Copy link
Contributor

I just read back through this and the previous issue, as I never responded to your last example there which you have expanded on here.

I agree that this works!

While it's true that this particular case becomes more verbose, I think that's actually a good thing. The mutual recursion case is very hard to wrap your head around, and having explicit starting points helps make it a lot more clear what is happening.

Here's my last example from the other issue, updated based on your current proposal, just to make sure I'm reading things correctly (We'll just pretend like there's a 2020-12 hyper-schema even though there isn't):

{
  "$id": "https://json-schema.org/draft/2020-12/links",
  "$dynamicAnchor": "links-schema",
  "type": "object",
  "properties": {
    "submissionSchema": { "$dynamicRef": "meta" }
  },
  "$defs": {
    "defaultDialect": {
      "$dynamicAnchor": "meta",
      "$ref": "https://json-schema.og/2020-12/hyper-schema#meta"
    }
  }
}
{
  "$id": "https://json-schema.org/draft/2020-12/hyper-schema",
  "$dynamicAnchor": "meta",
  "type": "object",
  "properties": {
    "items": {"$dynamicRef": "meta"},
    "links": {
      "type": "array",
      "items": {"$dynamicRef": "links-schema"}
    }
  },
  "$defs": {
    "defaultLinksSchema": {
      "$dynamicAnchor": "links-schema",
      "$ref": "https://json-schema.og/2020-12/links#links-schema"
    }
  }
}

@jdesrosiers I see that your solution accomplishes several things compared to 2020-12:

  • No bookending requirement (which I realize was done as a PR for the previous issue)
  • $ref/$anchor/$id and $dynamicRef/$dynamicAnchor are now totally independent sets of functionality
  • It is still possible to make a concrete schema involving $dynamicRef, you just have to explicitly bridge the URI-based static reference and identification system to the non-URI-based dynamic one

I definitely find that last point appealing. TBH I'm not sure why I thought the two systems should interact.

I like it! Definitely an improvement 😃

@jdesrosiers
Copy link
Member Author

@handrews Thanks for looking this over. Your timing is perfect because I was hoping to get back to working on this issue soon. Your example is pretty much what I had in mind except there should be no need for the $refs to include the fragment.

What are your thoughts about making dynamic anchors URIs? My main concern is that people will find it confusing even tho there's precedence in the web space (link relations).

We could take this a step further and make dynamic anchors URIs instead of plain name identifiers. Because dynamic anchors aren't scoped to the local document like traditional anchors, there's a possibility of name conflicts between schemas, so using URI identifiers would be safer. It's a similar concept to using URIs for link relations (for those who are familiar with those).

@handrews
Copy link
Contributor

there should be no need for the $refs to include the fragment

Oh, right- at least I think I understand. There are actually two things here:

  1. The target of these particular $refs is the resource root, so there's no need for a fragment at all
  2. If we did need a fragment, it would need to be declared with $anchor and not $dynamicAnchor because $dynamicAnchor no longer creates fragments (which I think is a good thing).

Are both of those correct? I think in some circumstances it would be fine to declare both a static $anchor target and a dynamic $dynamicAnchor target in the same schema, but agree it is unnecessary and probably confusing in this example.


What are your thoughts about making dynamic anchors URIs? My main concern is that people will find it confusing even tho there's precedence in the web space (link relations).

I think it will be confusing simply because you write the same dynamic anchor in multiple places, and that's not how URIs are normally used. It's fine with link relations, because you aren't targeting them in those different locations. They serve as references to or identifiers for the semantics of the link relation type. The target that they identify is the specification, and there's only one of those for each link relation URI.

Making $dynamicAnchor a URI makes that URI both a reference (to the semantics of the dynamic anchor) and a target (because you would then use it as the value of $dynamicRef) at the same time. And that's confusing. It's like making the href from <a href="https://example.com/whatever"> a target. The only way you can make an <a> element a target is <a href="https://example.com/whatever" id="thisIdIsAFragment"> But in that case, it's not the href that's the target, it's the id.

I think it's better to keep $dynamicAnchor and $dynamicRef separate from URIs/IRIs entirely. If the concern is uniqueness in an open ecosystem, there are other ways to handle that such as various sorts of namespacing. There's the Java approach of using reversed domain names, for example.

With link relation types, there is a globally shared notion of semantics. That's not really the same for $dynamicAnchor. While we name them after what they do for convenience, the names are just used mechanically. A schema with all of its human-readable $dynamicAnchor values consistently replaced with gibberish hex strings would work just as well. That is not true of link relation types, hence their need for a global semantic registry identified through URIs.

@jdesrosiers
Copy link
Member Author

  1. The target of these particular $refs is the resource root, so there's no need for a fragment at all

Yep

  1. If we did need a fragment, it would need to be declared with $anchor and not $dynamicAnchor because $dynamicAnchor no longer creates fragments (which I think is a good thing).

I think we could go either way with this. We could still allow dynamic anchors to be referenced by $ref, but this change would make that unnecessary and it would be cleaner to keep the two concepts separate; you reference anchors with $ref and dynamic anchors with $dynamicRef.

What are your thoughts about making dynamic anchors URIs? My main concern is that people will find it confusing even tho there's precedence in the web space (link relations).

I think it will be confusing ...

Yeah, you make some good points. I think it's not likely to be needed in enough cases to be worth the added complexity and there are plenty of ways to namespace a dynamic anchor that are compatible with plain-name fragments for the cases where it is needed.

@handrews
Copy link
Contributor

I'm in favor of making this change for the next release. We do need to figure out our compatibility story here, as the semantics of $dynamicAnchor change, and both the syntax and semantics of $dynamicRef, even though the core use case remains the same. We could:

  • Just change it — we're not guaranteeing compatibility yet anyway
  • make $dynamicAnchor2 and $dynamicRef2 or some other new pair of names

Just changing it would be more appealing if we can definitely automatically migrate existing uses to a combination of the new syntax and $ref (as some use cases require both a lexical and dynamic reference working together).

Of course, all of this is relevant to / in the context of #1242 .

@jdesrosiers
Copy link
Member Author

I think we can just change it. We aren't committed to no backwards compatible changes yet. This is definitely something we want to get in before we consider this feature stable.

@gregsdennis
Copy link
Member

I'm also in favor of just changing it.

I think a summary is warranted, though. From what I can see

  • $dynamicAnchors can currently be targeted using $ref and this won't be the case going forward. (Does anyone use this?) The $dynamic* keywords henceforth exist in a distinct referencing system.
  • $dynamicAnchors will no longer be required to exist in the same schema resource as $dynamicRef (bookending requirement is gone).

Is that correct? Am I missing anything?

@handrews
Copy link
Contributor

@gregsdennis yes to both of those points. Pulling in some additional details from the behaviors slides, I think this boils down to:

  • $dynamicAnchor no longer defines a URI fragment (this is why $ref could target it - it's just a URI, nothing special about that part), it defines names that are unique to it
  • $dynamicAnchor only associates its value with the dynamic scope if the same value is not already present in any parent dynamic scope (this avoids the need to search all the way out to find the farthest — I vaguely recall that you do something like this? or some implementation does, I'm pretty sure)
  • $dynamicRef can only target $dynamicAnchor names (no # URI fragment syntax, just the same name present for $dynamicAnchor)
  • $dynamicRef can only resolve to $dynamicAnchor values that are currently present in the dynamic scope, without regard to lexical scope (so, no need to reference an initial location, which I think was already done in PR Remove bookending requirement for dynamicRef #1139, and it is not possible to target a lexical scope through which evaluation has not passed (but you can combine dynamic and regular references to get that effect)

@jdesrosiers
Copy link
Member Author

To be clear, the bookending requirement was already removed in #1139. This proposal is in addition to that change.

@handrews
Copy link
Contributor

handrews commented Sep 2, 2022

Not 100% sure this proposal can replace this example:

{
  "$id": "https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main",
  "if": {
    "$id": "first_scope",
    "$defs": {
      "thingy": {
        "$comment": "this is first_scope#thingy",
        "$dynamicAnchor": "thingy",
        "type": "number"
      }
    }
  },
  "then": {
    "$id": "second_scope",
    "$ref": "start",
    "$defs": {
      "thingy": {
        "$comment": "this is second_scope#thingy, the final destination of the $dynamicRef for then",
        "$dynamicAnchor": "thingy",
        "type": "null"
      }
    }
  },
  "else": {
    "$id": "third_scope",
    "$ref": "start",
    "$defs": {
      "thingy": {
        "$comment": "this is third_scope#thingy, the final destination of the $dynamicRef for else",
        "$dynamicAnchor": "thingy",
        "type": "null"
      }
    }
  },
  "$defs": {
    "start": {
      "$comment": "this is the landing spot from $ref",
      "$id": "start",
      "$dynamicRef": "inner_scope#thingy"
    },
    "thingy": {
      "$comment": "this is the first stop for the $dynamicRef",
      "$id": "inner_scope",
      "$dynamicAnchor": "thingy",
      "type": "string"
    }
  }
}

It might be necessary to change $dynamicAnchor as proposed here, but have a 2-part $dynamicRef, e.g. "$dynamicRef": ["dynamicAnchorName", "uriReferece"] where dynamicAnchorName name is used to resolve the point in the dynamic scope chain, and then the uriReference is resolved against the base URI of the schema object associated with the dynamic scope. I think that would cover it, but be more clear than the current approach which kinda hides the base URI change aspect in a weird way.

@jdesrosiers
Copy link
Member Author

What about this example do you think won't work? The else is dead code and can be removed. The initial resolution to "inner_scope" exists only to satisfy the bookending requirement, so that can be removed too. When you clean that up, it seems like a pretty straightforward example. Let me know if I missed the point.

{
  "$id": "https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main",
  "if": {
    "$id": "first_scope",
    "$defs": {
      "thingy": {
        "$dynamicAnchor": "thingy",
        "type": "number"
      }
    }
  },
  "then": {
    "$id": "second_scope",
    "$ref": "start",
    "$defs": {
      "thingy": {
        "$dynamicAnchor": "thingy",
        "type": "null"
      }
    }
  },
  "$defs": {
    "start": {
      "$id": "start",
      "$dynamicRef": "thingy"
    }
  }
}

@handrews
Copy link
Contributor

handrews commented Sep 7, 2022

@jdesrosiers does $dynamicAnchor associate its anchor with the schema object or the schema resource? In 2020-12, it associates with the schema resource (because URI fragments are resource-scoped, because that's how URIs work). My assumption was that with dynamic anchors and references no longer using URIs, the anchors are schema object-scoped, although it seems like perhaps that is not what you meant.

Let's look at what I thought would happen, what I now think you are proposing, and another option that has come to mind for me. For all of these I put four locations at the beginning of each numbered step:

  • document-relative schema location: JSON Pointer fragment relative to the document's URI of https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main
  • canonical schema location (relative to https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/ for brevity)
  • instance location as a JSON Pointer (always "", but included because... reasons? I just felt like it? 😬)
  • evaluation path as a JSON Pointer

The last three are the bits of info from the output unit, as I'm sure you know but other readers may not. The document-relative one is for my sanity while tracking all of the $ids 😅

For all of these, the implementation steps are not necessarily what an implementation would have to do, just how I am thinking of it - any alternate implementation that produces the same outcome would be fine.

What I thought this was proposing

This is the behavior described in my presentation (original color; accessible color).

  • $dynamicAnchor only possibly takes effect when the schema object in which it appears is evaluated
  • $dynamicAnchor only actually takes effect if no $dynamicAnchor of the same name is present in a parent dynamic scope
  • $dynamicRef looks up the nearest matching $dynamicAnchor in the parent dynamic scopes, and resolves to the schema object associated with that dynamic scope

With this in mind, evaluation looks like:

  1. "#" | "main#" | "" | ""
    • no assertions so we start with the if applicator
  2. "#/if" | "first_scope#" | "" | "/if"
    • no assertions, but $id makes this an embedded resource
    • the $dynamicAnchor under document location #/if/$defs/thingy has no effect
  3. "#" | "main#" | "" | ""
    • we pop the "#/if" dynamic scope off the stack and move to the then applicator
  4. "#/then" | "second_scope#" | "" | "/then"
    • no assertions, but $id makes this an embedded resource, and we have a $ref to apply
    • the $dynamicAnchor under document location #/then/$defs/thingy has no effect
  5. #/$defs/start | "start#" | "" | "/then/$ref"
    • no assertions, but $id makes this an embedded resource, and we have a $dynamicRef to apply
    • "$dynamicRef": "thingy" cannot be resolved because no "$dynamicAnchor": "thingy" has been evaluated

What I now think you are proposing

I'm thinking about this in a 2-pass system- again, that doesn't mean it must be implemented this way.

Loading pass:

  • $id maps absolute URIs to schema resources (as always)
  • $anchor maps full URIs with plain name fragments to schema objects (as always)
  • $dynamicAnchor maps canonical base URIs to names and schema objects (JSON Pointers relative to the base URI) as follows:
    • https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/first_scope => (thingy, #/$defs/thingy)
    • https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/second_scope=> (thingy, #/$defs/thingy)

Given these mappings, evaluation would look like:

  1. "#" | "main#" | "" | ""
    • no assertions so we start with the if applicator
  2. "#/if" | "first_scope#" | "" | "/if"
    • no assertions, but $id makes this an embedded resource
  3. "#" | "main#" | "" | ""
    • we pop the "/if" dynamic scope off the stack and move to the then applicator
  4. "#/then" | "second_scope#" | "" | "/then"
    • no assertions, but $id makes this an embedded resource, and we have a $ref to process
  5. #/$defs/start | "start#" | "" | "/then/$ref"
    • no assertions, but $id makes this an embedded resource, and we have a $dynamicRef to process
    • current dynamic scope "/then/$ref" containing "$dynamicRef": "thingy" has canonical base URI https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/start, which is not present in the dynamic anchor mapping
    • parent dynamic scope 1 "/then" has canonical base URI https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/second_scope, which is in the dynamic anchor mapping
    • parent dynamic scope 2 "" has canonical base URI https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main which is not in the dynamic anchor mapping
    • the dynamic scope "/if" was popped, so it is not relevant
    • this means that "/then"is the farthest dynamic scope with an anchor mapping, leading to...
  6. #/then/$defs/thingy | "second_scope#/$defs/thingy" | "" | "/then/$ref/$dynamicRef"
    • asserts "type": "null"

This works, but I find the implicit combination of lexical and dynamic behavior in the $dynamicAnchor mapping to be too unintuitive (as is the case with similar the 2020-12 behavior). It's an improvement over $dynamicAnchor as a fragment, though, because separating the dynamic anchor name behavior from URI fragments means that at leat we're not abusing URI semantics anymore. So I could accept this approach, but I'll also propose an alternative.

An approach that explicitly separates behaviors

This approach has $dynamicAnchor behave as I'd expected (only associates and identifier when the schema object is evaluated, and even then onlhy if the same identifier does not already exist in the stack), and gives $dynamicRef a dual value: a dynamic anchor name for the dynamic lookup to find the appropriate dynamic scope, and a URI reference resolved against the schema location URI for the schema object associated with that dynamic scope for the lexical portion.

This requires a slightly different schema layout, which I think is more intuitive to read. I've used $anchor here because it retains the location-independent aspects, but this could be done without it.

{
  "$id": "https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main",
  "if": {
    "$id": "first_scope",
    "$dynamicAnchor": "x",
    "$defs": {
      "thingy": {
        "$anchor": "thingy",
        "type": "number"
      }
    }
  },
  "then": {
    "$id": "second_scope",
    "$dynamicAnchor": "x",
    "$ref": "start",
    "$defs": {
      "thingy": {
        "$anchor": "thingy",
        "type": "null"
      }
    }
  },
  "$defs": {
    "start": {
      "$id": "start",
      "$dynamicRef": ["x", "#thingy"]
    }
  }
}

With this approach, the loading pass is normal with only $id and $anchor processed, and the evaluation steps are:

  1. "#" | "main#" | "" | ""
    • no assertions so we start with the if applicator
  2. "#/if" | "first_scope#" | "" | "/if"
    • no assertions, but $id makes this an embedded resource
    • $dynamicAnchor assigns x to this dynamic scope as it is not in the stack
  3. "#" | "main#" | "" | ""
    • we pop the "#/if" dynamic scope off the stack and move to the then applicator
  4. "#/then" | "second_scope#" | "" | "/then"
    • no assertions, but $id makes this an embedded resource, and we have a $ref to apply
    • $dynamicAnchor assigns x to this dynamic scope as it is not in the stack (because the "/if" dynamic scope was popped)
  5. #/$defs/start | "start#" | "" | "/then/$ref"
    • no assertions, but $id makes this an embedded resource, and we have a $dynamicRef to apply
    • we look for the nearest "x" dynamic identifier from the first position in the $dynamicRef value, which we find in dynanic scope "/then"
    • we resolve the "#thingy" URI fragment from the second position in the $dynamicRef value against the schema location URI associated with that dynamic scope, resulting in https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/second_scope#thingy
    • this results in...
  6. #/then/$defs/thingy | "second_scope#/$defs/thingy" | "" | "/then/$ref/$dynamicRef"
    • asserts "type": "null"

This has the same result as the previous approach, but makes the dual dynamic + static lookup explicit in the reference, rather than implicit in a mapping.

Resolving $id and $anchor through resource-scope mappings is fine because that's how URIs work in general. We can point to analogous processes in HTML and elsewhere. But the two step lookup required to handle $dynamicAnchors by way of resource scopes do not have a convenient analogue for explanation. Therefore I think it is better to make the two setps explicit.

I had somehow thought previously that the two steps could be separated by using combintations of $ref and $dynamicRef, but that would require targeting a $dynamicAnchor that has an adjacent $ref, and if you can only get to a $dynanmicAnchor if you evaluate it, then you can't have a $ref there. It either won't line up at all or you end up in an infinite loop.

@jdesrosiers
Copy link
Member Author

@handrews
I found all of that very difficult to follow. I think what you now think I'm proposing is correct, although I'm really only certain that the end result is the same. I think you have a very different mental model of this than I do, but I'm pretty sure they are equivalent.

I especially have no idea what you mean by mixing lexical and dynamic behavior. Your alternate approach just seems to complicate the concept. I can't see the benefit.

I see $dynamicAnchor as the same concept as $anchor except that $anchor is only in the scope of the current resource while $dynamicAnchor stays in scope throughout evaluation and the first occurrence wins if there is a conflict. A $dynamicRef is then just a look up of the value of the dynamic anchor that is in scope and go to where it points. Changing $dynamicAnchor from schema resource scoped to schema object scoped means it doesn't behave like an $anchor anymore, which I think makes the concept more confusing and harder to reason about.

Here's a technical walk-through similar to what you presented. I only share in hopes that it helps communicate my mental model and thus improve communication. As with your example, the implementation details aren't important, as long as the behavior is the same.

  1. When the compound schema is loaded, it's broken down into its four schema resources and its dynamic anchors are identified.
dynamicAnchors = {
  ".../main": {},
  ".../first_scope": { thingy: "/$defs/thingy" },
  ".../second_scope": { thingy: "/$defs/thingy" }
  ".../start": {}
}
  1. Before we start evaluation we have the empty set of dynamic anchors in scope: {}.
  2. We start evaluation in ".../main", so we add ".../main"'s dynamic anchors to our current scope. Since ".../main" has no dynamic anchors, our current scope is still {}.
  3. Then we evaluate /if, which is actually moving to a different schema resource, ".../first_scope", so we need to add the dynamic anchors from ".../first_scope" to the current scope and get { thingy: ".../first_scope#/$defs/thingy" }.
  4. Nothing happens in /if, so we pop back to ".../main" where the scope is still {}.
  5. Then we evaluate then which is actually moving to ".../second_scope", so we add the dynamic anchors from ".../second_scope" and get { thingy: ".../second_scope#/$defs/thingy" }.
  6. Then we follow the $ref to end up in ".../start" and add it's dynamic anchors to the scope, which is empty so we still have, { thingy: ".../second_scope#/$defs/thingy" }. If ".../start" did have a "thingy" dynamic anchor, the original would take precedence.
  7. Then we evaluate the $dynamicRef. We look up "thingy" in the scope and follow it's value like a normal reference.

@handrews
Copy link
Contributor

@jdesrosiers thanks for working through my last comment despite the difficulty. I'm sorry it was not more clear or helpful.

Here's a technical walk-through similar to what you presented.

I believe what you're saying is identical to what I laid out in the "What I now think you are proposing", except for implementation details that are unimportant if not irrelevant.

Changing $dynamicAnchor from schema resource scoped to schema object scoped means it doesn't behave like an $anchor anymore, which I think makes the concept more confusing and harder to reason about.

That's a very good point. Arguably, since $dynamicAnchor no longer creates URI fragments with this proposal, it has already diverged in a significant way, in which case perhaps we should consider another set of keyword names. But we can consider whether a name change is necessary or appropriate later.

I especially have no idea what you mean by mixing lexical and dynamic behavior. Your alternate approach just seems to complicate the concept. I can't see the benefit.

If there is a benefit (and TBH there may not be), it's that it makes the inherent complexity of the behavior explicit rather than implicit. I'm going to explain this in the hopes that it fosters understanding of our divergent mental models, rather than as an argument that we need to adopt the "$dynamicRef": ["x", "#thingy"] approach.

To me, there are three aspects of $dynamic* in 2020-12, and it is the combination of them that makes it difficult to understand.

  1. Using URIs, which produces a confusing overlap in functionality and behavior with $ref and $anchor. Your proposal eliminates this aspect.
  2. The actual dynamic behavior of searching based on dynamic scopes, which is relatively (2019-09+) new for JSON Schema to have at all.
  3. The fact that dynamic anchors are resource-scoped (a lexical behavior), meaning that even though $dynamicRef is specified in terms of searching dynamic scopes, you have to consider the dynamic anchors for the entire resource at each dynamic scope rather than just the schema object through which evaluation passed

I personally find the combination of points 2 and 3 to be confusing and unintuitive. I originally thought that you were removing aspect 3, which dramatically simplified the behavior by making $dynamicRef purely dynamic, and relegating all lexical/static behavior to $ref. But that, as noted in prior comments, doesn't really work (and wasn't what you proposed anyway).

The "$dynamicRef": ["x", "#thingy"] syntax separates point 2 (which is handled entirely by the "x" value, which can only resolve to a "$dynamicAnchor": "x" through which evaluation directly passed) from point 3 (which is handled entirely by "#thingy", which is resolved against the base URI of the schema object where the "$dynamicAnchor": "x" was found).

Because what I would like is something like the following (which is not at all possible as written, and which I am not proposing. It is kind of like JSON Schema pseudo-code... psuedo-schema?). I'm going to use $setJump and $longJump for the dynamic side of things because the point here is that, as with C's setjmp, you can only jump to code that has actually been executed once already. This is just to make the difference from both the current $dynamic* and your proposal clear.

{
  "$id": "https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main",
  "if": {
    "$id": "first_scope",
    "$setJump": {
        "jumpAnchor": "x",
        "onJump": {"$ref": "#thingy"}
    },
    "$defs": {
      "thingy": {
        "$anchor": "thingy",
        "type": "number"
      }
    }
  },
  "then": {
    "$id": "second_scope",
    "$setJump": {
        "jumpAnchor": "x",
        "onJump": {"$ref": "#thingy"}
    },
    "$ref": "start",
    "$defs": {
      "thingy": {
        "$anchor": "thingy",
        "type": "null"
      }
    }
  },
  "$defs": {
    "start": {
      "$id": "start",
      "$longJump": "x"
    }
  }
}

Here it should be clear that "$longJump": "x" looks through the schema objects (not resources) in the dynamic scope for a "$setJump" with a "jumpAnchor" of "x" (the dynamic behavior). Rather than resolving to the schema object containing the "$setJump", it resolves to the schema under "onJump", which in this case does a normal "$ref" to "#thingy".

Again, I'm not suggesting this - it's even more verbose and requires duplicating the "onJump" schema in every "$setJump".

But it does show how I conceptualize $dynamicRef resolution as a two-step process, the first of which is dynamic, and the second of which is lexical. "$dynamicRef": ["x", "#thingy"] was an attempt to explicitly specify the two steps in a more concise manner.

However, if I'm the only one who finds that implicit behavior combination difficult, then there is no benefit to making it explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

3 participants