-
-
Notifications
You must be signed in to change notification settings - Fork 273
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
Remove $dynamicRef bookending requirement #1064
Comments
Here's an example of a non-recursive use for Hyper-schemers will recognize this pattern. Let's say I have several resources and for each resource, I want two schemas: one for a single instance and another for a list of instances. The list of instances follows a common pattern that looks something like this. {
"$id": "https://json-schema.hyperjump.io/schema/list",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"list": { "type": "array" },
"nextPage": { "type": "integer" },
"previousPage": { "type": "integer" },
"perPage": { "type": "integer" },
"page": { "type": "integer" }
}
} I can then compose this schema with another to create a list schema for a specific resource. {
"$id": "https://json-schema.hyperjump.io/schema/foo-list",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": "/schema/list",
"properties": {
"list": {
"items": { "$ref": "/schema/foo" }
}
}
} That's not too bad, but it does couple this schema with the "list" property name. If I decided I want to change that name to "itemList", I would have to change not only {
"$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": "#list" }
},
"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": "list",
"$ref": "/schema/foo"
}
}
} It's a little awkward, but it's decoupled. I can now change whatever I want about the However, this doesn't work yet because it doesn't fulfill the bookending requirement. According to the way the spec is written, "$defs": {
"list": { "$dynamicAnchor": "list" }
} but that's just putting the |
Here's what the hyper-schema related meta-schemas might look like if there was 2020-12 hyper-schema release. I've truncated them down to only what is necessary for this demo. {
"$id": "https://json-schema.org/draft/2020-12/hyper-schema",
"$dynamicAnchor": "meta",
"allOf": [
{ "$ref": "https://json-schema.org/draft/2020-12/schema" },
{ "$ref": "https://json-schema.org/draft/2020-12/meta/hyper-schema"}
]
} {
"$id": "https://json-schema.org/draft/2020-12/schema",
"$dynamicAnchor": "meta"
} {
"$id": "https://json-schema.org/draft/2020-12/meta/hyper-schema",
"$dynamicAnchor": "meta",
"properties": {
"links": {
"items": { "$ref": "#/$defs/links" }
}
},
"$defs": {
"links": {
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
}
}
} So far there's nothing surprising happening. Now, let's say we want to split {
"$id": "https://json-schema.org/draft/2020-12/meta/hyper-schema",
"$dynamicAnchor": "meta",
"properties": {
"links": {
"type": "array",
"items": { "$ref": "/draft/2020-12/links" }
}
}
} {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
} Without the bookending requirement, we'd be done and happy. But, because of the bookending requirement, we need to jump through some hoops to make this work. We need to use the absolute-URI portion of the URI to change the starting point to a schema that has a {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "/draft/2020-12/hyper-schema#meta" }
}
} The |
I've pointed out that if we remove the bookending requirement, there's never a need for a non-fragment-only URI. @ssilverman pointed out there's no reason for it to be a URI at all any more. I think that's exactly the direction this should be going. I think the default behavior of falling back to behaving like a It would be best if |
I agree wholeheartedly with your argument. I would only suggest that if a |
I disagree. If I try to reference something (static or dynamic) that doesn't exist, I expect that to be an error. Imagine the case where I typo a dynamic reference. It would be surprising and would mask the error if it just continues on with no indication of why the schema I thought I was referencing seems to be getting ignored.
It's a feature to be able to create an extensible schema that can't be used on it's own. It's like declaring an abstract class. There are good reasons to want to do that sometimes. And there's nothing stopping you from declaring a default, empty schema the same way I did in the first example. You'd just be doing it because you want a default, not because bookending said you have to. |
OK, that makes sense. The possibility of an abstract extensible schema sells it for me. :-) |
Yeah this could almost certainly all be better. For some reason, I really wanted all references to use RFC 3986 URI resolution somehow, and make sense in terms of base URIs and relative reference resolution. I could probably figure out what that reason was, but it might have just been because it's the kind of consistency I like. At this point, anything that makes it more clear without being less functional would be good. If |
I too wanted dynamic references to be URIs, but after after implementing it, explaining it, discussing it, and seeing it abused, I'm convinced that it's just not a good fit. The best solution I could come up with was something like this, {
"$dynamicScope": "foo",
"$ref": "#/$defs/bar"
}
|
OK, finally had time to go over this whole issue again in more detail. @jdesrosiers your first example is similar to one I'm dealing with for a client right now, so that makes sense to me (although the coupling involved would be substantially more than a single property name). I had also tried putting in a dummy {
...
"properties": {
"list": {
"type": "array",
"items": {
"$dynamicAnchor": "list",
"$dynamicRef": "#list"
}
},
...
} As it's very clear that this needs something somewhere to resolve it. But it also needs implementations not to infinitely recurse if they spot this and there's no dynamic anchor of the correct name in scope! However, your {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
} The whole point of this separate schema was to allow people to use the Hyper-Schema links format outside of JSON Hyper-Schema. I am not actually aware of anyone doing that these days, and the decision pre-dates me so I'm fuzzy on what use case might have been offered, but that's the point of that file. Which means that sometimes, the links schema is the entry point schema, and there is no other resource in the dynamic scope. You must choose a starting target in this case. That doesn't mean there needs to be a bookend, exactly, but you can't just use I remember wrestling with whether it should start at the default schema or hyper-schema. Either way has some sub-optimal effects, but it makes more sense to tie it to hyper-schema. Probably. I think. Anyway, that's not the point. If you have a |
@handrews Thank you for taking the time to review this in detail. This is an important issue to me and I haven't been able to get many people involved in this discussion outside of slack.
I doesn't work by itself, but it's not supposed to. It's effectively an abstract schema. Using the links schema as an entry point doesn't make sense. You have to provide something with a "meta" dynamic anchor. If you try to use it by itself, you should get an error because "meta" doesn't exist in the dynamic scope. It's no different than a Here's an example of describing an envelope-style media type that includes "headers", "message", and "links" where "links" are borrowed from hyper-schema. {
"$id": "https://example.com/custom-media-type",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"headers": {
"type": "string",
"additionalProperties": { "type": "string" }
},
"message": {},
"links": { "$ref": "https://json-schema.org/draft/2020-12/links" }
},
"$defs": {
"meta": {
"$dynamicAnchor": "meta",
"$ref": "https://json-schema.org/draft/2020-12/schema"
}
}
} The "meta" anchor doesn't even need to point to something that is a JSON Schema dialect. It can be something else entirely and it would still work. |
@jdesrosiers I see your perspective here, but I'm afraid I disagree. It should definitely work. You should not be forbidden from using a schema with a The dynamic scope starts with the initial target URI. That is part of why the bookending exists. What you have sold me on is the idea that we shouldn't require a resolvable initial target URI. There are clearly also use cases where there is no valid default resolution of the reference. But sometimes there is, and that should work, too. (and yeah, I feel you on not being able to get a discussion going on these really thorny keywords, and I appreciate your persistence on this) |
I guess this is also part of why I wanted it to all be URIs, beyond just the consistency. It was the most reasonable way to resolve the initial target. Of course, there's no reason we can't define analogous behavior with some other mechanism that is more clearly about dynamic resolution. |
@handrews I'm glad we agree on at least some of this. I'm a little confused about what exactly your preference is. I think you said you are ok with the list schema example which has |
@jdesrosiers these are definitely confusing cases! Essentially, the links schema points to a usable This means that links can be used as an entry point (the reference will always resolve), while lists cannot be used as an entry point (the reference will be unresolved without a different entry point supplying a target). Both cases should work. It's a bit like giving a parameter a default value. It's valid to do that, and it's also valid to require a value to be passed. |
I think we must be talking about different things. Here are the list schema and the links schema together for reference. They use exactly the same pattern. I don't see how one can be ok and the other not. {
"$id": "https://json-schema.hyperjump.io/schema/list",
"properties": {
"list": {
"type": "array",
"items": { "$dynamicRef": "#list" }
}
}
} {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
}
Why? What is lost by not allowing for abstract schemas? I've shown where abstract schemas allow for constraints that can't be expressed otherwise in the list schema example, so I'd need a really good reason to be convinced that they shouldn't be supported. If the opposition has anything to do with concerns about whether or not it can work in a clear, consistent, and predictable way, I can tell you that I've implemented it and know that it works great and is very easy to implement. Why should a |
I stated that you should not be forbidden from using non-abstract schemas (entry point schemas with a |
Well, then I have no idea what argument you're trying to make.
My definition of an abstract schema is a schema that can't be used as an entry point schema. You need to fill in the missing part to make it whole. I see no other way to create an abstract schema than to have a |
@jdesrosiers I am 100% on board with your usage of abstract schemas. I am also talking about a separate use case: the dynamic, non-abstract schemas that use a Can you help me understand how these two separate use cases conflict? To me, you could:
I am trying to demonstrate that we can and should allow both. |
That sounds like this example. {
"$id": "https://json-schema.hyperjump.io/schema/list",
"properties": {
"list": {
"type": "array",
"items": { "$dynamicRef": "#list" }
},
},
"$defs": {
"list": { "$dynamicAnchor": "list" }
}
}
There is no conflict. You can define non-abstract schemas just as easily as abstract schemas. So, I guess the next question is why do you think this proposal doesn't allow for non-abstract schemas? |
This is what I am responding to:
There is definitely a need for non-fragment-only URIs in |
You're going to have to spell that one out for me with an example. I see no way that the links schema needs a non-fragment-only URI. The example shows how removing the bookending requirement removes that need. Are you saying that something in the links example doesn't work the way I think it will? I assure you that it does. I've implemented it and made absolutely sure that it works. |
OK, so if I apply this schema: {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
} to the instance {
"submissionSchema": {
"type": "integer"
}
} what happens? |
Error: Referenced schema not found Same as this schema {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$ref": "/not/a/path/to/a/real/schema" }
}
} |
Right. Because in order for a non-abstract dynamic schema to be usable, you need to support this: {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "hyper-schema#meta" }
}
} You can't do that with a fragment-only reference. |
And really there's more to this, because the links schema and the hyper-schema meta-schema are mutually recursive, and should both be extensible: {
"$id": "https://json-schema.org/draft/2020-12/links",
"$dynamicAnchor": "links-schema",
"type": "object",
"properties": {
"submissionSchema": { "$dynamicRef": "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#links-schema"}
}
}
} You can enter this system from either schema document, so both need to use a non-fragment-only This is exactly the situation that motivated dumping |
Ahhh, now I see what you're talking about. You would do it the same way as you would with the links schema example. {
"$id": "https://json-schema.org/draft/2020-12/links",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
},
"$defs": {
"defaultDialect": {
"$dynamicAnchor": "#meta",
"$ref": "hyper-schema"
}
}
} So, you don't need non-fragment-only references, but I can see why you might want it in this case. I think that brings this issue down to preference. Do we keep |
I don't see any reason for the hyper-schema schema to be that complicated. This works, {
"$id": "https://json-schema.org/draft/2020-12/links",
"type": "object",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
} {
"$id": "https://json-schema.org/draft/2020-12/hyper-schema",
"$dynamicAnchor": "meta",
"type": "object",
"properties": {
"links": {
"type": "array",
"items": { "$ref": "links" }
}
}
} |
Hyper-schema does not work that way, as both the meta-schema and the links schema must be extensible, and in your most recent formulation, the links schema is not. Mutually recursive extensible things are not uncommon- I'm working with one right now for a client. It's fairly similar- there's a type of extensible thing (A), and there is another extensible thing (B) such that instances of B are used to describe relationships among instances of A. I bring this up to avoid fixating on the specific example of the links schema. It is the mutually recursive extensible pattern that matters. I will respond to your other comment shortly. |
Ok, I can see why you would want that. I'll try that case and make sure it's working in my implementation. |
This works with my implementation. {
"$schema": "https://json-schema.org/draft/future/schema",
"$id": "https://json-schema.org/draft/2020-12/hyper-schema",
"$dynamicAnchor": "meta",
"type": "object",
"properties": {
"items": {"$dynamicRef": "#meta"},
"links": {
"type": "array",
"items": {"$dynamicRef": "#links"}
}
},
"$defs": {
"links": {
"$dynamicAnchor": "links",
"$ref": "links"
}
}
} {
"$schema": "https://json-schema.org/draft/future/schema",
"$id": "https://json-schema.org/draft/2020-12/links",
"$dynamicAnchor": "links",
"type": "object",
"properties": {
"submissionSchema": { "$dynamicRef": "#meta" }
}
} |
This issue was originally about removing the bookending requirement and we did get agreement that that was something we wanted to do. We've yet to come to an agreement about the additional proposed changes that also ended up getting discussed. So, I've created PR #1139 for removing the bookending requirement and Issue #1140 to continue discussion about further changes to dynamic references. |
The current process for resolving a
$dynamicRef
requires that both ends of the dynamic scope have a$dynamicAnchor
. This bookending requirement complicates$dynamicRef
resolution, makes some use cases impossible, and doesn't seem to serve any useful purpose as far as I can tell.The only explanation for the bookending requirement in the spec comes from this cref.
But, that doesn't make sense. I don't see how
$anchor
could effect the dynamic resolution process nor what effect bookending could have in stabilizing the process.I'll follow up shortly with a couple examples of how the bookending requirement is harmful.
(This was originally reported in #1030 and split off here)
The text was updated successfully, but these errors were encountered: