Skip to content

Commit 866be5f

Browse files
authored
Merge pull request #16 from gregsdennis/logic/functional
update vocab docs
2 parents ec763fb + 40192f4 commit 866be5f

File tree

1 file changed

+77
-60
lines changed

1 file changed

+77
-60
lines changed

_docs/schema/vocabs.md

Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,69 +12,79 @@ A vocabulary is just a collection of keywords. It will be identified by a URI a
1212

1313
Creating a vocabulary in *JsonSchema.Net* isn't strictly required in order to add custom keywords, but if you're using it to create a meta-schema that will consume and validate other draft 2019-09 or later schemas, it is strongly suggested.
1414

15-
## How vocabularies work {#schema-vocabs-how-it-works}
15+
## Defining a vocabulary
1616

17-
This is best explained with an example. Suppose we have a meta-schema **M**, a schema **S** that uses **M** as its `$schema`, and a couple instances **I1** and **I2** to be validated by **S**.
17+
To make all of this work, we need a few things:
1818

19-
```jsonc
20-
// meta-schema M
19+
- A vocab schema that provides the syntax requirements for any new keywords defined by the vocabulary.
20+
- A vocab URI ID, this is different from the vocab schema's `$id` value.
21+
- A meta-schema that includes a `$vocabulary` keyword that references the vocab ID (along with any other vocabs you want to include) and a `$ref` keyword that references any vocab schemas (or meta-schemas that already include them).
22+
23+
This is best explained with an example. Let's suppose we want to define new keywords `minDate` and `maxDate`.
24+
25+
We'll start by defining our vocab schema:
26+
27+
```json
28+
{
29+
"$schema": "https://json-schema.org/draft/2020-12/schema",
30+
"$id": "https://myserver.net/meta/dateMath",
31+
"properties": {
32+
"minDate": {
33+
"type": "string",
34+
"format": "date"
35+
},
36+
"maxDate": {
37+
"type": "string",
38+
"format": "date"
39+
}
40+
}
41+
}
42+
```
43+
44+
> If you're defining keywords that contain subschemas (e.g. `allOf` or `properties`), you'll want to add a `"$dynamicAnchor": "meta"` to the root schema and then use `{ "$dynamicRef": "#meta" }` where you need schemas.
45+
{: .prompt-hint }
46+
47+
For the vocab URI ID, we'll use `https://myserver.net/vocab/dateMath`.
48+
49+
And then we need a meta-schema to tie everything together:
50+
51+
```json
2152
{
22-
"$schema": "https://json-schema.org/draft/2020-12/schema", // 1
53+
"$schema": "https://json-schema.org/draft/2020-12/schema",
2354
"$id": "https://myserver.net/meta-schema",
2455
"$vocabulary": {
25-
"https://json-schema.org/draft/2020-12/vocab/core": true, // 2
56+
"https://json-schema.org/draft/2020-12/vocab/core": true,
2657
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
2758
"https://json-schema.org/draft/2020-12/vocab/validation": true,
2859
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
2960
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
3061
"https://json-schema.org/draft/2020-12/vocab/content": true,
3162
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
32-
"https://myserver.net/my-vocab": true
63+
"https://myserver.net/vocab/dateMath": true
3364
},
34-
"allOf": [ // 3
35-
{ "$ref": "https://json-schema.org/draft/2020-12/schema" }
36-
],
37-
"properties": {
38-
"minDate": { // 4
39-
"type": "string",
40-
"format": "date"
41-
}
42-
}
65+
"allOf": [
66+
{ "$ref": "https://json-schema.org/draft/2020-12/schema" },
67+
{ "$ref": "https://myserver.net/meta/dateMath" }
68+
]
4369
}
70+
```
4471

45-
// schema S
46-
{
47-
"$schema": "https://myserver.net/meta-schema", // 5
48-
"$id": "https://myserver.net/schema",
49-
"properties": {
50-
"publishedOnDate": {
51-
"minDate": "2019-01-01" // 6
52-
}
53-
}
54-
}
72+
> It's not always necessary to have a vocab schema. If you decide to not have one, then any meta-schema evaluations won't be able to validate your keyword's syntax. Maybe your keyword doesn't require any specific syntax... or validating syntax isn't important to you.
73+
{: .prompt-info }
5574

56-
// instance I1
57-
{
58-
"publishedOnDate": "2019-06-22" // 7
59-
}
60-
// instance I2
61-
{
62-
"publishedOnDate": "1998-06-22" // 8
63-
}
64-
```
75+
## How this works
6576

66-
1. We declare a meta-schema. In this case, it's the draft 2020-12 meta-schema. This will validate our schema and declare the set of rules it should be processed with.
67-
2. We list the vocabularies that the *JsonSchema.Net* should know about in order to process schemas that declare this meta-schema as their `$schema` (see #5). This includes all of the vocabularies from 2020-12 (because we want all of the 2020-12 capabilities) as well as the vocab for this meta-schema. We'll explain a bit more about this later.
68-
3. We also need all of the syntactic validation from 2020-12, so we include it in an `allOf`.
69-
4. We define a new keyword, `minDate`, that takes a date-formatted string value.
70-
5. We create a schema that uses our new meta-schema (because we want to use the new keyword).
71-
6. We use the new keyword to define a property to be found in the instance.
72-
7. The first instance defines a date after the minimum required date.
73-
8. The second date defines a date before the minimum required date.
77+
When a schema evaluator loads a schema that uses our meta-schema in its `$schema` keyword, it loads the meta-schema and looks at the `$vocabulary` keyword to determine the set of vocabularies which the meta-schema uses.
7478

75-
The kicker here is that we can read "minDate" and know that its value represents the minimum acceptable date... because we're human, and we understand things like that. However, a validator isn't going to be able to infer that. It can only validate that a date-formatted string was given for `minDate` in the schema (**S**).
79+
That set of vocabularies specifies which keywords to process, defining a "dialect". Any keywords that are not defined by the dialect have their values collected as annotations (or they're ignored).
7680

77-
That's where the vocabulary comes in. The vocabulary is a human-readable document that gives *semantic* meaning to `minDate`. It is documentation of business logic that allows a programmer to code an extension that provides additional validation. For example, this is the documentation for `minLength` in the Schema Validation specification:
81+
The catch is that while we can read "minDate" and know that its value represents the minimum acceptable date... because we're human, and we understand things like that, a validator isn't going to be able to infer what a keyword is supposed to do by its name or the subschema that defines its syntax. It can only validate that the schema uses a date-formatted string.
82+
83+
That's where the vocabulary specification comes in.
84+
85+
## Defining keyword functionality
86+
87+
The vocabulary specification is a human-readable document that gives *semantic* meaning to the vocab's keywords. It is documentation of business logic that allows a programmer to code an extension that provides additional validation. For example, this is the documentation for `minLength` in the Schema Validation specification:
7888

7989
> **6.3.2. minLength**
8090
>
@@ -88,35 +98,31 @@ That's where the vocabulary comes in. The vocabulary is a human-readable docume
8898
8999
It gives meaning to the keyword beyond how the meta-schema describes it: a non-negative integer.
90100

91-
Any validator can validate that `minDate` is a date-formatted string, but only a validator that understands `https://myserver.net/my-vocab` as a vocabulary will understand that `minDate` should validate that a date in the instance should be later than that in the schema.
101+
Any validator can validate that `minDate` is a date-formatted string, but only a validator that understands `https://myserver.net/vocab/dateMath` _as a vocabulary_ will understand that `minDate` should validate that the date in the instance should be later than the date in the schema.
92102

93-
Now, if you look at the `$vocabulary` entry for `https://myserver.net/my-vocab`, the vocabulary has its ID as the key with a boolean value. In this case, that value is `true`. That means that if *JsonSchema.Net* *doesn't* know about the vocabulary, it **must** refuse to process any schema that declares **M** as its `$schema` (as **S** does). If this value were `false`, then *JsonSchema.Net* would be allowed to continue, which means that only syntactic analysis (i.e. "Is `minDate` a date-formatted string?") would be performed.
94-
95-
So, back to the example, because we declare the vocabulary to be required (by giving it a value of `true`) *and* because *JsonSchema.Net* knows about it, **I1** is reported as valid and **I2** is not. If the vocabulary had not been required and *JsonSchema.Net* didn't know about the vocabulary, both **I1** and **I2** would be reported as valid because the `minDate` keyword would not have been enforced.
103+
Now, if you look at the `$vocabulary` entry for `https://myserver.net/vocab/dateMath`, the vocabulary has its ID as the key with a boolean value. In this case, that value is `true`. That means that if the evaluator *doesn't* know about the vocabulary, it **must** refuse to process any schema that uses our meta-schema. If this value were `false`, then the validator would be allowed to continue, but it would only be able to collect the keyword's value as an annotation (or ignore it).
96104

97105
## Registering a vocabulary {#schema-vocabs-registration}
98106

99-
To tell *JsonSchema.Net* about a vocabulary, you need to create a `Vocabulary` instance and register it using `VocabularyRegistry.Add<T>()`.
107+
To tell *JsonSchema.Net* about a vocabulary, you need to create a `Vocabulary` instance and register it using `VocabularyRegistry.Register<T>()`.
100108

101109
The `Vocabulary` class is quite simple. It defines the vocabulary's ID and lists the keywords which it supports.
102110

103-
The keywords must still be registered separately (see "Defining Custom Keywords" below).
104-
105-
It's not always necessary to have a meta-schema for your vocabulary. However, if you want to enable `EvaluationOptions.ValidateMetaschema`, you will need to register it.
111+
The keywords must still be registered separately (keep reading for instructions on creating and registering keywords).
106112

107113
## Defining Custom Keywords {#schema-vocabs-custom-keywords}
108114

109-
`JsonSchema` has been designed to allow you to create your own keywords. There are several steps that need to be performed to do this.
115+
_JsonSchema.Net_ has been designed with custom keywords in mind. There are several steps that need to be performed to do this.
110116

111117
1. Implement `IJsonSchemaKeyword`.
112-
1. Optionally implement one of the schema-container interfaces.
118+
2. Optionally implement one of the schema-container interfaces.
113119
1. `ISchemaContainer`
114120
2. `ISchemaCollector`
115121
3. `IKeyedSchemaCollector`
116122
4. `ICustomSchemaCollector`
117-
2. Apply some attributes.
118-
3. Register the keyword.
119-
4. Create a JSON converter.
123+
3. Apply some attributes.
124+
4. Register the keyword.
125+
5. Create a JSON converter.
120126

121127
And your new keyword is ready to use.
122128

@@ -132,7 +138,7 @@ Both stages are defined by implementing the single method on `IJsonSchemaKeyword
132138

133139
_JsonSchema.Net_ v6 includes updates to support [Native AOT applications](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). Please be sure to read the main AOT section on the [overview page](/schema/basics#aot).
134140

135-
First, you'll need to add `[JsonSerializable]` attributes for any custom keywords.
141+
First, on your serialization context, you'll need to add `[JsonSerializable]` attributes for any custom keywords.
136142

137143
```c#
138144
[JsonSerializable(typeof(MyKeyword))]
@@ -183,7 +189,7 @@ Here, getting the constraint means just pointing to the evaluation function, whi
183189

184190
Once the constraints have all been collected, _JsonSchema.Net_ will move on to the evaluation phase, which creates an "evaluation" object for each constraint, which contains things that are specific to the current evaluation, including the local instance being evaluated, any options (which include the schema and vocabulary registries), and the local results object.
185191

186-
For `maximum`, evaluation means we check if the value is a number. If not, we indicate that the keyword doesn't apply by calling `.MarkAsSkipped()`. (This tells _JsonSchema.Net_ that that any nested results don't need to be added.) If the instance is a number, and it doesn't meet the requirement, then we fail the keyword with an error.
192+
For `maximum`, evaluation means we check if the value is a number. If not, we indicate that the keyword doesn't apply by calling `.MarkAsSkipped()`. (This tells _JsonSchema.Net_ that that any nested results don't need to be added to the output.) If the instance is a number, and it doesn't meet the requirement, then we fail the keyword with an error.
187193

188194
> `maximum` doesn't have any nested results, but it's still good form to explicitly indicate this.
189195
{: .prompt-info }
@@ -317,6 +323,17 @@ public static JsonSchemaBuilder Description(this JsonSchemaBuilder builder, stri
317323
}
318324
```
319325

326+
You might also want to create a keyword-access extension method on `JsonSchema`. This provides an easy, safe way to get a keyword's value, if it exists. Here's the extension method for getting the `description` keyword value:
327+
328+
```c#
329+
public static string? GetDescription(this JsonSchema schema)
330+
{
331+
return schema.TryGetKeyword<DescriptionKeyword>(DescriptionKeyword.Name, out var k)
332+
? k.Value
333+
: null;
334+
}
335+
```
336+
320337
### 5. Create a JSON converter {#schema-vocabs-custom-converter}
321338

322339
To enable serialization and deserialization, you'll need to provide the converter for it.

0 commit comments

Comments
 (0)