proposal: a replacement for $dynamicRef
and $dynamicAnchor
#789
Replies: 6 comments 32 replies
-
Thanks for writing this up. I would very much like to see a replacement for dynamic references that isn't dynamic. I think this is theoretically sound and solves the problems it needs to solve. Since the one thing this feature needs to do is support meta-schema composition for dialect construction, I wrote up a short example and it looks like it should work to me. {
"$id": "https://example.com/dialect",
"$schema": "https://json-schema.org/next",
"$vocabulary": {
"https://example.com/vocab/A": true,
"https://example.com/vocab/B": true
},
"type": ["object", "boolean"],
"allOf": [
{
"$ref": "https://example.com/meta/A",
"$refParams": {
"meta": { "$ref": "#" }
}
},
{
"$ref": "https://example.com/meta/B",
"$refParams": {
"meta": { "$ref": "#" }
}
}
]
} {
"$id": "https://example.com/meta/A",
"$schema": "https://json-schema.org/next",
"$params": {
"meta": {}
},
"properties": {
"foo": {
"type": "object",
"patternProperties": {
"": { "$ref": "#/$refParams/meta" }
}
}
}
} {
"$id": "https://example.com/meta/B",
"$schema": "https://json-schema.org/next",
"$params": {
"meta": {}
},
"properties": {
"bar": {
"type": "array",
"items": { "$ref": "#/$refParams/meta" }
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Although I think this theoretically works, I'm holding back from getting too excited until it's implemented because that's usually when any issues come to light. My main concern is that the However, it seems to me that the same goals could be achieved without this reference duality problem. The type constraint can be implemented using normal schema composition methods without needing to support reference duality and all that might imply. In this example, I remove {
"$id": "https://example.com/generic-tree",
"type": "array",
"items": {
"anyOf": [
{ "$paramRef": "branch" },
{
"type": ["string", "number", "boolean", "null"],
"$paramRef": "leaf"
}
]
}
} {
"$id": "https://example.com/bounded-string-tree",
"$ref": "https://example.com/generic-tree",
"$refParams": {
"branch": {
"$ref": "#",
"maxItems": 2
},
"leaf": { "type": "string" }
}
} Hopefully, that makes sense. |
Beta Was this translation helpful? Give feedback.
-
I think my primary hesitation with this proposal in its current state is that it's being touted as a non-dynamic replacement for |
Beta Was this translation helpful? Give feedback.
-
Unbounded instantiation cyclesThis is following up from the footnote in this comment. Here's an example of a recursive schema that seems legitimate under this proposal:
This would validate against the instance
but fail against
because there aren't enough We can't tell how many schemas are going to be instantiated until the schema is actually checked against the instance. I think this is problematic: it means that the core of any evaluator has to retain the ability to instantiate new schemas, which limits the space of possible evaluator implementations. In particular, it's not possible to entirely translate the schema to another form that just does checking without instantiating new schemas. I see this as closely related to a similar problem in compiled languages with generic types that monomorphise the types at compilation time. See this StackOverflow question for an example of the issue. I think we should apply a similar restriction to this proposal: a recursive type definition that leads to arbitrary expansion of schemas should be disallowed, or at least documented to be undefined behaviour. That is, in principle it should be possible to statically transform a schema with I outlined a sketch of a possible algorithm for doing that in the above comment. I found one example (and explanation) of an algorithm for doing the cycle check in the Go compiler here. I believe it shouldn't be too tricky to implement such a check for JSON Schema. I believe that the ability to do this transformation into a parameterless form would justify the description of this feature as a a non-dynamic replacement for |
Beta Was this translation helpful? Give feedback.
-
Parameter references in formal parametersHere's another edge case that needs to be considered: whether to allow parameter references inside parameter schemas themselves. An easy initial answer might be to forbid this, but it also might provide some useful expressive power, so worth not dismissing the possibility out of hand. For example:
|
Beta Was this translation helpful? Give feedback.
-
I've been thinking more about this proposal, and I think what it does at its core is replace the need to track What this also means is that parameters are defined at the schema level instead of at the resource level. If you want branching behavior, you can do it all within the same resource. No more strategically defining There are a couple behavioral things from the original proposal that I'd to align with 1. Add a "first wins" behavior
So in this case: {
"$params": {
"foo": { "type": "integer" }
},
"type": "array",
"items": {
"type": "object",
"$params": {
"foo": { "type": "string" }
},
"properties": {
"bar": { "$paramRef": "foo" }
}
}
} The The original proposal stated:
But this creates an "overriding" behavior which is contrary to how 2. Parameter "popping" (like a stack)As evaluation "backs out" of a schema resource, it drops any {
"$id": "base",
"$defs": {
"ref-target": {
"$id": "target",
"$comment": "Both anyOf subschemas point here",
"$dynamicRef": "#definition",
"$defs": {
"undefined": {
"$comment": "This is required with 2020-12, but we're removing that requirement for future releases",
"$dynamicAnchor": "definition",
"not": true
},
}
}
},
"anyOf": [
{
"$id": "first",
"$ref": "target",
"$defs": {
"resolved-for-first": {
"$dynamicAnchor": "definition",
"type": "string"
}
}
},
{
"$id": "second",
"$ref": "target",
"$defs": {
"resolved-for-second": {
"$dynamicAnchor": "definition",
"type": "number"
}
}
}
]
} Assuming an iterative approach, when the evaluation path passes through If we make {
"$defs": {
"target": {
"$comment": "Both anyOf subschemas point here",
"$paramRef": "definition"
}
},
"anyOf": [
{
"$ref": "#/$defs/target",
"$params": {
"definition": {
"type": "string"
}
}
},
{
"$ref": "base#/$defs/target",
"$params": {
"definition": {
"type": "number"
}
}
}
]
} I think the primary thing that sticks out to me is that we've been able to remove the need to manage With these two additions to the proposed behavior, I think we have a functional replacement for |
Beta Was this translation helpful? Give feedback.
-
[Edited to use
$paramRef
rather than$ref
]Motivation
This discussion was prompted by this Slack thread, which I somewhat repeat here for context.
Like many before me I'm sure, I've been trying to understand
$dynamicRef
and `$dynamicAnchor . I found this article illuminating: https://json-schema.org/blog/posts/dynamicref-and-genericsBut that left me with some questions that I was struggling to resolve:
The article demonstrates how to represent a generic
List<T>
type, but it seems that it's not possible to represent the equivalent ofList<List<T>>
, because there's only one dynamic anchor with a given fragment name available in a given dynamic scope (the outermost one) but nesting the generic list needs two, each with a different value.Related, but not quite the same, the spec says:
So the fact that $dynamicAnchor is defined with respect to a particular base URL is immaterial. But it also means that all dynamic anchors exist in the same global namespace, which could be a problem. As a trivial example, say, someone used the generic list definition above to define a
list-of-string
schema:Then we want to use that
list-of-string
schema as part of some other generic schema that also usesT
for its dynamic anchor name:This doesn't work (code here doesn't support sharing), and there's no way to say "I want to define this dynamic anchor for a particular absolute URI" because
$dynamicAnchor
can only be used to create a name fragment, not a full URL.One pertinent reply in the thread was this from @gregsdennis:
This seems slightly at odds with the examples of the feature that have been provided in various places, notably Appendix C of the spec, also in the above mentioned blog article and in this StackOverflow reply, all of which describe what are essentially generic data structures.
This all made me think that perhaps the current design isn't ideal and could use an overhaul to make it more fit for general use and less vulnerable to unexpected behaviour: it works fine in the limited metaschema world, but I'm not entirely convinced it's a good design when considered in the wider schema context.
So here are some thoughts as to what such a new feature might look like. The goal here is to provide something that is:
$dynamic*
in the metaschemaProposal
Note: this has been written down rather quickly by someone (me!) without a deep understanding of the spec and various real-world implementations, but hopefully might be good as a starting point.
Parameterized schemas with
$params
,$refParams
and$paramRef
The
$params
keyword provides a way for a schema to allow other schemas to be provided to it as parameters. The value of$params
MUST be an object. Each member of this object MUST be a valid JSON schema and declares a parameter that can be passed to the schema.A parameter can be used by referring to it by name using the
$paramRef
keyword. The value of$paramRef
MUST be a string. It refers to a parameter declared with nearest enclosing$params
keyword. Informally, it validates if both the parameter schema (in$params
) validates and the actual parameter bound to it with$refParams
validate. More formally, if the schema declared in$params
isP
and the schema bound to the parameter isS
, the results of$paramRef
are results of:Parameters are passed by using the
$refParams
keyword. The value of$refParams
MUST be an object. Each member of this object MUST be a valid JSON schema. If the$refParams
keyword is present, the$ref
keyword must also be present and the schema referred to MUST define parameters (with$params
) with names that match the member names in the$refParams
value. Each member of$refParams
is bound to the correspondingly named member of$params
in the referred-to schema.For example, a simple schema to define a structure for adding metadata information to an object might be described as follows:
This might be used as follows:
The above schema would validate against the following instance:
but would fail to validate the following instance because the
_metadata
field is required by the metadata schema:It would also fail to validate against this instance, because the
other
property is not allowed by the metadata-user schema.Discussion
As written,
$ref
is used both to refer to a schema parameter and a regular schema. This allows schema parameters to be passed to schemas that are parameters themselves, which might be seen to be similar to higher-kinded polymorphism in some other languages, and might make validator implementation more complex. It might be better to use a different keyword to refer to a schema parameter, leaving$refParams
to operate exclusively on$ref
and ruling out this possibility. In this case, the value of the field could defined to be the name of the parameter only rather than a URI.One omission here is the ability to specify whether any parameters are required. As written, all parameters are considered optional. This could potentially be remedied by providing a
requiredParams
field, or similar.Beta Was this translation helpful? Give feedback.
All reactions