Skip to content

Commit 8bfa375

Browse files
committed
Revisit $dynamicRef
Signed-off-by: Juan Cruz Viotti <[email protected]>
1 parent 8b25910 commit 8bfa375

File tree

1 file changed

+113
-127
lines changed

1 file changed

+113
-127
lines changed

content/2020-12/core/dynamicRef.markdown

Lines changed: 113 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -24,169 +24,155 @@ related:
2424
keyword: $defs
2525
---
2626

27-
The `$dynamicRef` keyword is a dynamic applicator that allows for runtime resolution of schema references. Unlike the static `$ref`, which resolves the referenced schema at schema load time, `$dynamicRef` defers full resolution until the instance is evaluated. It attempts to resolve the given fragment based on the dynamic scope at that given point in time.
28-
- This keyword is particularly useful for handling recursive schemas where the schema references itself or where the structure of the schema may change at runtime.
29-
- If the `$dynamicRef` includes more than just a fragment, the URI except for the fragment is statically resolved first, and then only the fragment is dynamically resolved.
30-
- Notably, official meta-schemas use this mechanism themselves for defining vocabularies!
27+
The [`$dynamicRef`]({{< ref "2020-12/core/dynamicref" >}}) keyword is an
28+
extension of the [`$ref`]({{< ref "2020-12/core/ref" >}}) keyword that enables
29+
a schema to reference another schema by its dynamic anchor, as declared by the
30+
[`$dynamicAnchor`]({{< ref "2020-12/core/dynamicanchor" >}}) keyword. When
31+
resolving a dynamic anchor using this keyword, the base URI of the origin is
32+
not considered. Instead, the evaluator looks in the [dynamic
33+
scope](https://json-schema.org/blog/posts/dynamicref-and-generics) and jumps to
34+
the first encountered occurence of the given dynamic anchor in the [stack of
35+
schema
36+
resources](https://json-schema.org/blog/posts/understanding-lexical-dynamic-scopes#the-dynamic-scope-as-a-stack)
37+
traversed so far.
38+
39+
In other words, **think of a schema declaring a dynamic reference as a
40+
reference that considers that its destination might have been re-defined by a
41+
parent schema**. For example, a schema that dynamically references an anchor
42+
`foo` says: _"jump to the location set by the `foo` anchor, but if there are
43+
overriden variants of it, jump to the first of those instead"_.
44+
45+
{{<best-practice>}}
46+
47+
Use dynamic references when you need to define generic and extensible schemas.
48+
In fact, the dynamic referencing mechanism is a direct translation of generic
49+
programming facilities like [C++ template
50+
parameters](https://en.cppreference.com/w/cpp/language/template_parameters) and
51+
[Java generics](https://en.wikipedia.org/wiki/Generics_in_Java) to JSON Schema.
52+
See the blog post [Using Dynamic References to Support Generic
53+
Types](https://json-schema.org/blog/posts/dynamicref-and-generics#using-dynamic-references-to-support-generic-types)
54+
by Greg Dennis (co-author of the JSON Schema specification) for a hands-on
55+
discussion of this concept.
56+
57+
{{</best-practice>}}
58+
59+
{{<learning-more>}}
60+
61+
The official JSON Schema meta-schemas define, by convention, a dynamic anchor
62+
called `meta`. This is a fundamental building block for schema extensibility.
63+
The meta-schema of every vocabulary (official or third-party) hooks into this
64+
dynamic anchor to extend the recursive definition of what constitutes a valid
65+
schema for the given dialect.
66+
67+
More specifically, by relying on the `meta` dynamic anchor, a vocabulary
68+
meta-schema can validate the presence of a new keyword and have those
69+
constraints be automatically discovered and applied by any applicator of any
70+
other vocabulary (even future ones).
3171

32-
{{<learning-more>}} URIs play a central role in JSON Schema. Going through the URI [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) specification is a must for gaining a deeper understanding of references, identifiers, and
33-
anchors. More specifically, we recommend carefully studying [URI resolution](https://datatracker.ietf.org/doc/html/rfc3986#section-5), URLs vs URNs, and the difference between a URI and a URI Reference.
34-
35-
You may also find these blog posts helpful for gaining a deeper understanding of dynamic references.
36-
* [Understanding JSON Schema Lexical and Dynamic Scopes](https://json-schema.org/blog/posts/understanding-lexical-dynamic-scopes)
37-
* [Using Dynamic References to Support Generic Types](https://json-schema.org/blog/posts/dynamicref-and-generics)
3872
{{</learning-more>}}
3973

4074
{{<common-pitfall>}}
41-
**Bookending:** The bookending requirement means that when you use a `$dynamicRef`, the JSON Schema processor needs to find a matching `$dynamicAnchor` within the target schema resource, even if the target is different from the destination schema. This ensures that the reference resolves correctly by matching the actual anchor defined in the target schema resource, preventing unresolvable references due to scope issues.
42-
{{</common-pitfall>}}
43-
44-
## Examples
45-
46-
{{<schema `After leaving a dynamic scope, '$dynamicAnchor' is not used by a '$dynamicRef'`>}}
47-
{
48-
"$schema": "https://json-schema.org/draft/2020-12/schema",
49-
"$id": "https://example.com/root",
50-
"if": {
51-
"$id": "firstScope",
52-
"$defs": {
53-
"thingy": {
54-
"$comment": "this is firstScope#thingy",
55-
"$dynamicAnchor": "thingy",
56-
"type": "number"
57-
}
58-
}
59-
},
60-
"then": {
61-
"$id": "secondScope",
62-
"$ref": "start",
63-
"$defs": {
64-
"thingy": {
65-
"$comment": "this is secondScope#thingy, the final destination of the $dynamicRef",
66-
"$dynamicAnchor": "thingy",
67-
"type": "null"
68-
}
69-
}
70-
},
71-
"$defs": {
72-
"start": {
73-
"$comment": "this is the landing spot from $ref",
74-
"$id": "start",
75-
"$dynamicRef": "innerScope#thingy"
76-
},
77-
"thingy": {
78-
"$comment": "this is the first stop for the $dynamicRef",
79-
"$id": "innerScope",
80-
"$dynamicAnchor": "thingy",
81-
"type": "string"
82-
}
83-
}
84-
}
85-
{{</schema>}}
8675

87-
{{<instance-fail `String matches '/$defs/thingy', but the '$dynamicRef' does not stop here`>}}
88-
"a string"
89-
{{</instance-fail>}}
90-
91-
{{<instance-fail `firstScope is not in dynamic scope for the '$dynamicRef'`>}}
92-
42
93-
{{</instance-fail>}}
94-
95-
{{<instance-pass `'/then/$defs/thingy' is the final stop for the '$dynamicRef'`>}}
96-
null
97-
{{</instance-pass>}}
76+
As a fallback, the specification allows the use of this keyword to reference
77+
static resources and non-dynamic anchors. However, to avoid confusion and keep
78+
your schemas easy to understand, don't rely on this fallback behaviour. Only
79+
make use of this keyword to reference dynamic anchors set by the
80+
[`$dynamicAnchor`]({{< ref "2020-12/core/dynamicanchor" >}}) keyword, without
81+
making use of any URI component (other than the fragment) as part of the
82+
reference.
9883

99-
- The evaluation begins with the top-level schema, where the dynamic scope is the root schema resource.
100-
- **Dynamic Scope:** `https://example.com/root`
84+
{{</common-pitfall>}}
10185

102-
- Upon encountering the `if` applicator, a new schema resource (`https://example.com/firstScope`) is declared and added to the stack, expanding the dynamic scope.
103-
- **Dynamic Scope:** `https://example.com/root` --> `https://example.com/firstScope`
86+
To debug which dynamic anchor the evaluation process is jumping to, try the
87+
[`jsonschema
88+
validate`](https://github.com/sourcemeta/jsonschema/blob/main/docs/validate.markdown)
89+
command with the `--trace` option. This option prints a trace of every step in
90+
the evaluation process alongside the corresponding keywords and their
91+
respective locations, letting you know which destination was preferred when
92+
encountering a dynamic reference. For example:
10493

105-
- Since `https://example.com/firstScope` doesn't reference any other schema resource, the evaluation of the `if` schema completes, and the stack unwinds, returning to the root schema resource.
106-
- **Dynamic Scope:** `https://example.com/root`
94+
```sh
95+
$ jsonschema validate string-list.json instance.json --resolve generic-list.json --trace
96+
...
10797

108-
- The successful validation by the `if` subschema triggers entry into the `then` applicator, introducing another schema resource (`https://example.com/secondScope`), thus extending the dynamic scope.
109-
- **Dynamic Scope:** `https://example.com/root` --> `https://example.com/secondScope`
98+
-> (push) "/$ref/items/$dynamicRef" (ControlDynamicAnchorJump)
99+
at "/0"
100+
at keyword location "https://example.com/generic-list#/items/$dynamicRef"
101+
at vocabulary "https://json-schema.org/draft/2020-12/vocab/core"
110102

111-
- Within the `then` subschema, a reference to another schema resource (`https://example.com/start`) further enriches the dynamic scope.
112-
- **Dynamic Scope:** `https://example.com/root` --> `https://example.com/secondScope` --> `https://example.com/start`
103+
-> (push) "/$ref/items/$dynamicRef/type" (AssertionTypeStrict)
104+
at "/0"
105+
at keyword location "https://example.com/string-list#/$defs/generic-list-item/type"
106+
at vocabulary "https://json-schema.org/draft/2020-12/vocab/validation"
113107

114-
- Additionally, within the `then` subschema, a dynamic reference is made to another schema resource (`https://example.com/innerScope#thingy`). While the initial part of the URI is resolved statically to `/$defs/thingy`, the inclusion of `#thingy` fragment reults in the final resolution to `/then/$defs/thingy` because the first dynamic anchor encountered in the current dynamic scope is at `/then/$defs/thingy`. So, only a null value is valid in this case.
108+
...
109+
```
115110

116-
**Note:** _The non-fragment part is always statically resolved, while the fragment may be dynamically resolved._
111+
## Examples
117112

118-
{{<schema `Schema with '$dynamicRef' and '$dynamicAnchor' keywords in the same schema resource`>}}
113+
{{<schema `A generic schema that describes an array where the items definition (by default anything) can be overriden through a dynamic anchor`>}}
119114
{
120115
"$schema": "https://json-schema.org/draft/2020-12/schema",
121-
"required": [ "name", "age", "address" ],
122-
"properties": {
123-
"name": { "$dynamicRef": "#name" },
124-
"age": { "$dynamicRef": "#age" },
125-
"address": { "$ref": "#address" }
116+
"$id": "https://example.com/generic-list",
117+
"type": "array",
118+
"items": {
119+
"$dynamicRef": "#generic-list-item"
126120
},
127121
"$defs": {
128-
"name": {
129-
"$dynamicAnchor": "name",
130-
"type": "string",
131-
"minLength": 3
132-
},
133-
"age": {
134-
"$anchor": "age",
135-
"type": "integer"
136-
},
137-
"address": {
138-
"$dynamicAnchor": "address",
139-
"type": "string",
140-
"maxLength": 50
122+
"default": {
123+
"$comment": "This is a default declaration to satisfy the bookending requirement",
124+
"$dynamicAnchor": "generic-list-item"
141125
}
142126
}
143127
}
144128
{{</schema>}}
145129

146-
{{<instance-pass `An instance adhering to the above schema is valid`>}}
147-
{
148-
"name": "John",
149-
"age": 35,
150-
"address": "1234 Elm Street, Springfield, IL 62701, USA"
151-
}
130+
{{<instance-pass `An empty array value is valid`>}}
131+
[]
152132
{{</instance-pass>}}
153133

154-
{{<instance-fail `Required properties must be present`>}}
155-
{ "name": "Doe", "age": 61 }
156-
{{</instance-fail>}}
134+
{{<instance-pass `An array value with arbitrary items is valid`>}}
135+
[ 1, "foo", false ]
136+
{{</instance-pass>}}
157137

158-
* _A `$dynamicRef` referencing a `$dynamicAnchor` within the same schema resource functions similarly to a standard `$ref` referencing an `$anchor`. Similarly, a `$dynamicRef` referencing an `$anchor` within the same schema resource behaves like a typical `$ref` referencing an `$anchor`. Likewise, a `$ref` targeting a `$dynamicAnchor` within the same schema resource behaves like a regular `$ref` targeting an `$anchor`._
138+
{{<instance-annotation>}}
139+
{ "keyword": "/items", "instance": "", "value": true }
140+
{{</instance-annotation>}}
141+
142+
{{<instance-fail `A non-array value is invalid`>}}
143+
"Hello World"
144+
{{</instance-fail>}}
159145

160-
{{<schema `Schema with multiple '$dynamicAnchor' set to same value`>}}
146+
{{<schema `A schema that specialises the previous generic schema to declare that array items must be strings`>}}
161147
{
162148
"$schema": "https://json-schema.org/draft/2020-12/schema",
163-
"$id": "https://example.com/root",
164-
"$ref": "list",
149+
"$id": "https://example.com/string-list",
150+
"$ref": "https://example.com/generic-list",
165151
"$defs": {
166-
"foo": {
167-
"$dynamicAnchor": "items",
152+
"generic-list-item": {
153+
"$dynamicAnchor": "generic-list-item",
168154
"type": "string"
169-
},
170-
"list": {
171-
"$id": "list",
172-
"items": { "$dynamicRef": "#items" },
173-
"$defs": {
174-
"items": {
175-
"$dynamicAnchor": "items",
176-
"type": "integer"
177-
}
178-
}
179155
}
180156
}
181157
}
182158
{{</schema>}}
183159

184-
{{<instance-pass `An instance adhering to the above schema is valid`>}}
185-
[ "foo", "bar" ]
160+
{{<instance-pass `An empty array value is valid`>}}
161+
[]
186162
{{</instance-pass>}}
187163

188-
{{<instance-fail `A non-string array is invalid`>}}
189-
[ 11, 22 ]
164+
{{<instance-fail `An array value with arbitrary items is invalid`>}}
165+
[ 1, "foo", false ]
190166
{{</instance-fail>}}
191167

192-
* _A `$dynamicRef` resolves to the first `$dynamicAnchor` still in scope that is encountered when the schema is evaluated._
168+
{{<instance-pass `An array value with string items is valid`>}}
169+
[ "foo", "bar", "baz" ]
170+
{{</instance-pass>}}
171+
172+
{{<instance-annotation>}}
173+
{ "keyword": "/$ref/items", "instance": "", "value": true }
174+
{{</instance-annotation>}}
175+
176+
{{<instance-fail `A non-array value is invalid`>}}
177+
"Hello World"
178+
{{</instance-fail>}}

0 commit comments

Comments
 (0)