diff --git a/.jekyll-metadata b/.jekyll-metadata index 29c9fa73..13c9ac7a 100644 Binary files a/.jekyll-metadata and b/.jekyll-metadata differ diff --git a/Gemfile b/Gemfile index 7b5377f1..58ae9eee 100644 --- a/Gemfile +++ b/Gemfile @@ -10,13 +10,13 @@ end # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem # and associated library. -platforms :mingw, :x64_mingw, :mswin, :jruby do +platforms :windows, :jruby do gem "tzinfo", ">= 1", "< 3" gem "tzinfo-data" end # Performance-booster for watching directories on Windows -gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] +# gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem # do not have a Java counterpart. diff --git a/_docs/pointer/basics.md b/_docs/pointer/basics.md index baeb2aef..bb0ba5b4 100644 --- a/_docs/pointer/basics.md +++ b/_docs/pointer/basics.md @@ -62,12 +62,8 @@ There are three ways create pointers: ```c# var pointer = JsonPointer.Create("object", "and", 3, "arrays"); ``` -- building with `Create()` and supplying a LINQ expression (also see [below](#linq)) - ```c# - var pointer = JsonPointer.Create(x => x.objects.and[3].arrays); - ``` -All of these options will give you an instance of the model that can be used to evaluate JSON data. +Both of these options will give you an instance of the model that can be used to evaluate JSON data. ```c# using var element = JsonDocument.Parse("{\"objects\":{\"and\":[\"item zero\",null,2,{\"arrays\":\"found me\"}]}}"); @@ -121,43 +117,16 @@ Get the immediate parent: ```c# var pointer = JsonPointer.Parse("/objects/and/3/arrays"); -var parent = pointer[..^1]; // /objects/and/3 +var parent = pointer.GetParent(); // /objects/and/3 ``` Or get the local pointer (imagine you've navigated to `/objects/and/` and you need the pointer relative to where you are): ```c# var pointer = JsonPointer.Parse("/objects/and/3/arrays"); -var local = pointer[^2..]; // /3/arrays +var local = pointer.GetLocal(2); // /3/arrays ``` -There are also method versions of this functionality, which are also available if you're not yet using .Net 8: `.GetAncestor(int)` and `.GetLocal()`. - -> Accessing pointers acts like accessing strings: getting segments has no allocations (like getting a `char` via the string's `int` indexer), but creating a sub-pointer _does_ allocate a new `JsonPointer` instance (like creating a substring via the string's `Range` indexer). -{: .prompt-info } - -### Building pointers using Linq expressions {#linq} - -When building a pointer using the `Create()` method which takes a Linq expression, there are a couple of things to be aware of. - -First, JSON Pointer supports using `-` as a segment to indicate the index beyond the last item in an array. This has several use cases including creating a JSON Patch to add items to arrays. - -Secondly, you have some name transformation options at your disposal. - -The first way to customize your pointer is by using the `[JsonPropertyName]` attribute to provide a custom name. Since this attribute controls how System.Text.Json serializes the property, this attribute will override any other options. - -The second way to customize your pointer is by providing a `PointerCreationOptions` object as the second parameter. Currently there is only the single option: `PropertyNamingResolver`. This property is a function that takes a `MemberInfo` and returns the string to use in the pointer. Several presets have been created for you and are available in the `PropertyNamingResolvers` static class: - -| Name | Summary | -|---|---| -| **AsDeclared** | Makes no changes. Properties are generated with the name of the property in code. | -| **CamelCase** | Property names to camel case (e.g. `camelCase`). | -| **KebabCase** | Property names to kebab case (e.g. `Kebab-Case`). | -| **PascalCase** | Property names to pascal case (e.g. `PascalCase`). | -| **SnakeCase** | Property names to snake case (e.g. `Snake_Case`). | -| **UpperKebabCase** | Property names to upper kebab case (e.g. `UPPER-KEBAB-CASE`). | -| **UpperSnakeCase** | Property names to upper snake case (e.g. `UPPER_SNAKE_CASE`). | - ## Relative JSON Pointers {#pointer-relative} [JSON Hyperschema](https://datatracker.ietf.org/doc/draft-handrews-json-schema-hyperschema/) relies on a variation of JSON Pointers called [Relative JSON Pointers](https://tools.ietf.org/id/draft-handrews-relative-json-pointer-00.html) that also includes the number of parent and/or array-index navigations. This allows the system to start at an internal node in the JSON document and navigate to another node potentially on another subtree. diff --git a/_docs/schema/basics.md b/_docs/schema/basics.md index ddaa04f1..a56aa226 100644 --- a/_docs/schema/basics.md +++ b/_docs/schema/basics.md @@ -5,7 +5,7 @@ md_title: _JsonSchema.Net_ Basics bookmark: Basics permalink: /schema/:title/ icon: fas fa-tag -order: "01.1" +order: "01.01" --- The occasion may arise when you wish to validate that a JSON object is in the correct form (has the appropriate keys and the right types of values), or perhaps you wish to annotate that data. Enter JSON Schema. Much like XML Schema with XML, JSON Schema defines a pattern for JSON data. A JSON Schema validator can verify that a given JSON object meets the requirements as defined by the JSON Schema as well as provide additional information to the application about the data. This evaluation can come in handy as a precursor step before deserializing. @@ -13,9 +13,18 @@ More information about JSON Schema can be found at [json-schema.org](http://json To support JSON Schema, *JsonSchema.Net* exposes the `JsonSchema` type. This type is implemented as a list of keywords, each of which correspond to one of the keywords defined in the JSON Schema specifications. -## Specification versions {#schema-versions} +> Keep this mantra in your head while using this library: **Build once; evaluate many times.** +{: .prompt-tip } + +## Keywords {#schema-keywords} + +JSON Schema is expressed as a collection of keywords, each of which provides a specific constraint on a JSON instance. For example, the `type` keyword specifies what JSON type an instance may be, whereas the `minimum` keyword specifies a minimum numeric value *for only numeric data* (it will not apply any assertion to non-numeric values). These keywords can be combined to express the expected shape of any JSON instance. Once defined, the schema evaluates the instance, providing feedback on what errors occurred, including where in the instance and in the schema produced them. + +_JsonSchema.Net_ implements keywords using singleton keyword handlers. These handlers are responsible for validating that the keyword data is valid in the schema, building any subschemas, and instance evaluation. -There are currently six drafts of the JSON Schema specification that have known use: +## Versions, Meta-schemas, and Dialects {#schema-versions} + +There are currently six versions of the JSON Schema specification that have known use: - Draft 3 - Draft 4 @@ -24,83 +33,78 @@ There are currently six drafts of the JSON Schema specification that have known - Draft 2019-09 - Draft 2020-12 -The JSON Schema team recommends using draft 7 or later. *JsonSchema.Net* supports draft 6 and later. +The JSON Schema team recommends using draft 7 or 2020-12. *JsonSchema.Net* supports draft 6 and later. -> The next version of JSON Schema, which is supported by v4.0.0 and later of this library, is currently in development and will start a new era for the project which includes various backward- and forward-compatibility guarantees. Have a read of the various discussions happening in the [JSON Schema GitHub org](https://github.com/json-schema-org) for more information. +> The next version of JSON Schema, v1/2026, which is also supported by this library, is currently in development and will start a new era for the project which includes various backward- and forward-compatibility guarantees. Have a read of the various discussions happening in the [JSON Schema GitHub org](https://github.com/json-schema-org) for more information. {: .prompt-tip } -> This library uses [`decimal`](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-8.0) for floating point number representation. While `double` (and even `float`) may support a larger range, the higher precision of `decimal` is often more important (for example, in financial applications). This also aligns with [JSON](https://datatracker.ietf.org/doc/html/rfc8259#section-6) itself, which uses arbitrary-precision numbers. [This site](https://www.geeksforgeeks.org/difference-between-decimal-float-and-double-in-net/) has a good summary of the differences between the numeric types. -{: .prompt-warning } - -### Meta-schemas {#schema-metaschemas} +Each of these specification versions define a _dialect_. A dialect is the specific group of keywords that a schema can use. A dialect usually has a URI identifier. A schema declares the dialect it's using by placing the dialect identifier in the `$schema` keyword. Generally, though not required, a dialect will be accompanied by a meta-schema that uses the same URI in its `$id`. -Each version defines a meta-schema. This is a special JSON Schema that describes all of the keywords available for that version. They are intended to be used to validate other schemas. Usually, a schema will declare the version it should adhere to using the `$schema` keyword. +A meta-schema is a special JSON Schema that syntactically describes all of the keywords available for the associated dialect. They are intended to be used to validate other schemas. -*JsonSchema.Net* declares the meta-schemas for the supported versions as members of the `MetaSchemas` static class. +Draft 2019-09 introduced the idea of vocabularies as re-usable collections of keywords, a kind of sub-dialect, if you will. A vocabulary isn't a dialect on its own, but they can be combined to create a dialect, as 2019-09 is. As part of this new feature, the meta-schemas for this version and those which follow it have been split into vocabulary-specific meta-schemas. -Draft 2019-09 introduced vocabularies. As part of this new feature, the meta-schemas for this version and those which follow it have been split into vocabulary-specific meta-schemas. Additionally, the specification recognizes that the meta-schemas aren't perfect and may need to be updated occasionally. As such, the meta-schemas defined by this library will be updated to match, in most cases only triggering a patch release. +The specification recognizes that the meta-schemas aren't perfect and may need to be updated occasionally. As such, the meta-schemas defined by this library will be updated to match, in most cases only triggering a patch release. +{: .prompt-info } -## Keywords {#schema-keywords} +In _JsonSchema.Net_, dialects are supported through the `Dialect` class, which is instantiated using a URI identifier and the keywords supported by that dialect, and meta-schemas. All of the dialects and meta-schemas assocated with supported JSON Schema specification versions are predefined by the library; you can also make your own. -JSON Schema is expressed as a collection of keywords, each of which provides a specific constraint on a JSON instance. For example, the `type` keyword specifies what JSON type an instance may be, whereas the `minimum` keyword specifies a minimum numeric value *for only numeric data* (it will not apply any assertion to non-numeric values). These keywords can be combined to express the expected shape of any JSON instance. Once defined, the schema evaluates the instance, providing feedback on what errors occurred, including where in the instance and in the schema produced them. +Since keyword behavior has evolved over the various specification versions, each different behavior for a given keyword has its own keyword handler. Customization of keyword behavior is done by creating new keyword handlers and supporting them through custom dialects. # Building a schema {#schema-build} -There are two options when building a schema: defining it inline using the fluent builder and defining it externally and deserializing. Which method you use depends on your specific requirements. +This library follows a two-phase approach to JSON Schema evaluation: build then evaluate. The build phase produces a abstract graph that represents the schema and attempts to resolve all references. Schemas are built using a selection of options that can be passed to the build process using the `BuildOptions` object. These options include the dialect you want to use and registries for schemas (to resolve references), dialects (for dialect auto-selection via `$schema`), and vocabularies (for handling 2019-09 and 2020-12 meta-schemas which declare a `$vocabulary` keyword). -## Serialization and Deserialization {#schema-deserialization} +There are two main ways to build a schema: parsing text into a `JsonElement` and passing it to `JsonSchema.Build()` and defining it inline using the fluent builder. (Serialization is also an option, but the converter merely extracts a `JsonElement` and builds directly.) -Serialization is how we convert between the textual representation of JSON Schema and a `JsonSchema` .Net object. In many cases, you'll compose your schemas in separate JSON files and deserialize them into the `JsonSchema` model. However if you [define your schemas in code](#schema-inlining) or [generate them from a type](/schema/schemagen/schema-generation/) you won't have a textual representation of those schemas on hand. - -To facilitate this, _JsonSchema.Net_ schemas are fully serializable. +> Because _JsonSchema.Net_ builds schemas directly from `JsonElement`, serialization has been mostly removed from the process of building schemas. Where serialization is performed, this library and its extensions do include support for [Native AOT applications](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). +{: .prompt-info } -```c# -var mySchema = JsonSchema.FromText(content); -``` +## Options -which just does +A schema is always based on the registries and dialect you provide it through the build options. ```c# -var mySchema = JsonSerializer.Deserialize(content); +var buildOptions = new BuildOptions +{ + Dialect = Dialect.Draft202012, + SchemaRegistry = new(), + DialectRegistry = new(), + VocabularyRegistry = new() +} ``` -Done. +All of these properties are optional. -> You can either use the JSON serializer as shown above, or the YAML serializer found in [_Yaml2JsonNode_](/yaml/basics/). -{: .prompt-tip} +The dialect you choose determines which properties will be recognized as keywords. The dialect also defines whether unknown keywords are allowed (the upcoming spec version will disallow unknown keywords) and whether `$ref` allows or ignores other keywords in the same schema object (drafts 6 & 7 cause `$ref` to ignore sibling keywords, so they're not processed). The default dialect is managed by `Dialect.Default`; out of the box, it's V1 to prepare for the upcoming specification release, but until then, it's recommended you set the default to Draft 2020-12. -### Ahead of Time (AOT) compatibility {#aot} +```c# +Dialect.Default = Dialect.Draft202012; +``` -_JsonSchema.Net_ v6 includes updates to support [Native AOT applications](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). In order to take advantage of this, there are a few things you'll need to do. +The registries are available so that you can keep your registrations separate or if you want to build the same schema under differing conditions. This can come in handy for concurrency or other scenarios where you might encounter conflicts or you need to rebuild the same schema under differing conditions. For most scenarios, you should be able to just use the global registries, which is the default. -First, on your `JsonSerializerContext`, add the following attributes: +> If you're using a custom meta-schema, you'll need to load it per the [Schema Registration](json-schema#schema-registration) section below. Custom meta-schemas form a chain of meta-schemas (e.g. your custom meta-schema may reference another which references the draft 2020-12 meta-schema). Ultimately, the chain MUST end at a JSON-Schema-defined meta-schema as this defines the processing rules for the schema. An error will be produced if the meta-schema chain ends at a meta-schema that is unrecognized. +{: .prompt-info } -```c# -[JsonSerializable(typeof(JsonSchema))] -[JsonSerializable(typeof(EvaluationResults))] -``` +## Via `JsonElement` -It's recommended that you create a single `JsonSerializerOptions` object (or a few if you need different configurations) and reuse it rather than creating them ad-hoc. When you create one, you'll need to configure its `TypeInfoResolverChain` with your serializer context: +The simplest way to build a schema is through the `JsonSchema.Build()` static method. ```c# -var serializerOptions = new() -{ - TypeInfoResolverChain = { MySerializerContext.Default } -}; +var schemaJson = JsonDocument.Parse("""{"type": "object"}""").RootElement; +var schema = JsonSchema.Build(schemaJson); // optionally include build options ``` -If you don't have any custom keywords, you're done. Congratulations. - -If you do have custom keywords, please see the AOT section on the [Vocabularies docs](/schema/vocabs#aot). +Building the schema this way will perform validation on the incoming schema data, handle building subschemas, and attempt to resolve references. It resolves as much behavior as it can up front in order to keep the evaluation step as quick as possible. -> The vocabulary library extensions for _JsonSchema.Net_ are also AOT-compatible and require no further setup. -{: .prompt-tip} +Both this approach and the following inline approach will auto-register the schema in the schema registry provided by the options. ## Inline {#schema-inlining} -There are many reasons why you would want to hard-code your schemas. This library actually hard-codes all of the meta-schemas. Whatever your reason, the `JsonSchemaBuilder` class is going to be your friend. +The `JsonSchemaBuilder` is a fluent approach to building schemas that uses a builder and extension methods to ensure that all keyword values are valid. This is a more type-safe way to build schemas, but it can also be a bit more verbose. The API has been crafted in an attempt to mimic the JSON representation of the schema. -The builder class itself is pretty simple. It just has an `.Add()` method which takes an instance of `IJsonSchemaKeyword`. The real power comes from the multitudes of extension methods. There's at least one for every keyword, and they all take the appropriate types for the data that the keyword expects. +The `JsonSchemaBuilder` class itself is pretty simple. It just has an `.Add()` method which takes an instance of `IJsonSchemaKeyword`. The real power comes from the multitudes of extension methods. There's at least one for every keyword, and they all take the appropriate types for the data that the keyword expects. Once you've added all of your properties, just call the `.Build()` method to get your schema object. @@ -111,77 +115,15 @@ var builder = new JsonSchemaBuilder() var schema = builder.Build(); ``` -Let's take a look at some of the builder extension methods. - -### Easy mode {#schema-how-to-1} - -Some of the more straightforward builder methods are for like the `title` and `$comment` keywords, which just take a string: - -```c# -builder.Comment("a comment") - .Title("A title for my schema"); -``` - -Notice that these methods implement a fluent interface so that you can chain them together. - -### A little spice {#schema-how-to-2} +Both the `JsonSchemaBuilder` constructor and the `.Build()` method can take a `BuildOptions` parameter. The options passed into the `.Build()` method takes priority over one passed into the constructor. -Other extension methods can take multiple values. These have been overloaded to accept both `IEnumerable` and `params` arrays just to keep things flexible. +Here's an example of creating a simple schema using the builder. ```c# -var required = new List{"prop1", "prop2"}; -builder.Required(required); -``` - -or just - -```c# -builder.Required("prop1", "prop2"); -``` - -### Now you're cooking with gas {#schema-how-to-3} - -Lastly, we have the extension methods which take advantage of C# 7 tuples. These include keywords like `$defs` and `properties` which take objects to mimic their JSON form. - -```c# -builder.Properties( - ("prop1", new JsonSchemaBuilder() - .Type(SchemaValueType.String) - .MinLength(8) - ), - ("prop2", new JsonSchemaBuilder() - .Type(SchemaValueType.Number) - .MultipleOf(42) - ) - ); -``` - -Did you notice how the `JsonSchemaBuilder` is just included directly without the `.Build()` method? These methods actually require `JsonSchema` objects. This leads us into the next part. - -### Conversions {#schema-implicit-cast} - -`JsonSchemaBuilder` defines an implicit cast to `JsonSchema` which calls the `.Build()` method. - -To help things further, `JsonSchema` also defines implicit conversions from `bool`. This allows you to simply use `true` and `false` to create their respective schemas. - -```c# -builder.Properties( - ("prop1", new JsonSchemaBuilder() - .Type(SchemaValueType.String) - .MinLength(8) - ), - ("prop2", new JsonSchemaBuilder() - .Type(SchemaValueType.Number) - .MultipleOf(42) - ), - ("prop3", true) - ); -``` - -This cast can be used anywhere a `JsonSchema` is needed, such as in the `additionalProperties` or `items` keywords. - -```c# -builder.Properties( +JsonSchema schema = new JsonSchemaBuilder() + .Schema(MetaSchemas.Draft202012Id) + .Type(SchemaValueType.Object) + .Properties( ("prop1", new JsonSchemaBuilder() .Type(SchemaValueType.String) .MinLength(8) @@ -199,6 +141,9 @@ builder.Properties( .AdditionalProperties(false); ``` +>`JsonSchemaBuilder` defines an implicit cast to `JsonSchema` which calls the `.Build()` method with the default options. To help things further, `JsonSchema` also defines implicit conversions from `bool`. This allows you to simply use `true` and `false` to create their respective schemas. +{: .prompt-tip } + # Evaluation & annotations {#schema-evaluation} Among the myriad of uses for JSON Schema, _JsonSchema.Net_ is considered a "validator". That is, it evaluates schemas against a data instance and produces a validation result and annotations. @@ -238,12 +183,12 @@ JsonSchema schema = new JsonSchemaBuilder() .Required("myProperty"); // you can build or parse you JsonNode however you like -var emptyJson = new JsonObject(); -var booleanJson = JsonNode.Parse("{\"myProperty\":false}"); -var stringJson = new JsonObject { ["myProperty"] = "some string" }; -var shortJson = new JsonObject { ["myProperty"] = "short" }; -var numberJson = new JsonObject { ["otherProperty"] = 35.4 }; -var nonObject = JsonNode.Parse("\"not an object\""); +var emptyJson = JsonDocument.Parse("{}").RootElement; +var booleanJson = JsonDocument.Parse("""{"myProperty":false}""").RootElement; +var stringJson = JsonDocument.Parse("""{"myProperty":"some string"}""").RootElement; +var shortJson = JsonDocument.Parse("""{"myProperty":"short"}""").RootElement; +var numberJson = JsonDocument.Parse("""{"otherProperty":35.4}""").RootElement; +var nonObject = JsonDocument.Parse("\"not an object\"").RootElement; var emptyResults = schema.Evaluate(emptyJson); var booleanResults = schema.Evaluate(booleanJson); @@ -253,9 +198,6 @@ var numberResults = schema.Evaluate(numberJson); var nonObjectResults = schema.Evaluate(nonObject); ``` -> Don't pass your JSON to `Evaluate()` as a string. You must parse it with `JsonNode.Parse()` first. Otherwise, your string will be implicitly cast to `JsonNode` and you're just validating a string instance. -{: .prompt-warning } - The various results objects are of type `EvaluationResults`. More information about the results object can be found in the next section. In the above example, the following would result: @@ -436,6 +378,9 @@ The default output format is Flag, but this can be configured via the `Evaluatio The `format` keyword has been around a while. It's available in all of the versions supported by *JsonSchema.Net*. Although this keyword is technically classified as an annotation, the specification does allow (the word used is "SHOULD") that implementation provide some level of validation on it so long as that validation may be configured on and off. +> In the upcoming JSON Schema v1 specification, `format` will validate by default. +{: .prompt-warning } + *JsonSchema.Net* makes a valiant attempt at validating a few of them. These are hardcoded as static fields on the `Formats` class. Out of the box, these are available: - `date` @@ -461,26 +406,16 @@ New formats must be registered via the `Formats.Register()` static method. This > Format implementations MUST not contain state as the same instance will be shared by all of the schema instances that use it. {: .prompt-warning } -## Options {#schema-options} - -> For the best performance, use a cached evaluation options object. -> -> *JsonSchema.Net* optimizes repeated evaluations with the same schema by performing some static analysis during the first evaluation. However because changes to evaluation options can affect this analysis, the analysis is recalculated if the options change or a new options object is detected. -{: .prompt-warning } +## Evaluation Options {#evaluation-options} The `EvaluationOptions` class gives you a few configuration points for customizing how the evaluation process behaves. It is an instance class and can be passed into the `JsonSchema.Evaluate()` method. If no options are explicitly passed, a copy of `JsonSchemaOptions.Default` will be used. -- `EvaluateAs` - Indicates which schema version to process as. This will filter the keywords of a schema based on their support. This means that if any keyword is not supported by this version, it will be ignored. This will need to be set when you create the options. -- `SchemaRegistry` - Provides a way to register schemas only for the evaluations that use this set of options. -- `ValidateAgainstMetaSchema` - Indicates whether the schema should be validated against its `$schema` value (its meta-schema). This is not typically necessary. Note that the evaluation process will still attempt to resolve the meta-schema. \* - `OutputFormat` - You already read about output formats above. This is the property that controls it all. By default, a single "flag" node is returned. This also yields the fastest evaluation times as it enables certain optimizations. - `RequireFormatValidation` - Forces `format` validation. -- `OnlyKnownFormats` - Limits `format` validation to only those formats which have been registered through `Formats.Register()`. Unknown formats will fail validation. -- `ProcessCustomKeywords` - For schema versions which support the vocabulary system (i.g. 2019-09 and after), allows custom keywords to be processed which haven't been included in a vocabulary. This still requires the keyword type to be registered with `SchemaRegistry`. - `PreserveDroppedAnnotations` - Adds a `droppedAnnotations` property to the output nodes for subschemas that fail validation. +- `AddAnnotationForUnknownKeywords` - Adds an `$unknownKeywords` annotation that lists the names of keywords in a subschema that were not known. - `IgnoredAnnotations` - Gets the set of annotations that will be excluded from the output. - -_\* If you're using a custom meta-schema, you'll need to load it per the [Schema Registration](json-schema#schema-registration) section below. Custom meta-schemas form a chain of meta-schemas (e.g. your custom meta-schema may reference another which references the draft 2020-12 meta-schema). Ultimately, the chain MUST end at a JSON-Schema-defined meta-schema as this defines the processing rules for the schema. An error will be produced if the meta-schema chain ends at a meta-schema that is unrecognized._ +- `Cuture` - Sets the culture to be used for error messages. ### Annotation management {#annotation-mgmt} @@ -517,11 +452,13 @@ options.CollectAnnotationsFrom(); By default, *JsonSchema.Net* handles all references as defined in the draft 2020-12 version of the JSON Schema specification. What this means for draft 2019-09 and later schemas is that `$ref` can now exist alongside other keywords; for earlier versions (i.e. Drafts 6 and 7), keywords as siblings to `$ref` will be ignored. +In _JsonSchema.Net_ this sibling-keyword behavior is controlled by the dialect that's used during the build step. For a new dialect, the default behavior is to allow sibling keywords to be processed. This can be disabled by setting the `RefIgnoresSiblingKeywords` property to `true`. + ## Schema resolution {#schema-ref-resolution} -In order to resolve references more quickly, *JsonSchema.Net* maintains two registries for all schemas and identifiable subschemas that it has encountered. The first is a global registry, and the second is a local registry that is contained in the options and is passed around on the evaluation context. If a schema is not found in the local registry, it will automatically search the global registry. +In order to resolve references more quickly, *JsonSchema.Net* maintains two registries for all schemas and identifiable subschemas that it has encountered. The first is a global registry, and the second is a local registry that is contained in the options and is passed around on the build context. If a schema is not found in the local registry, it will automatically search the global registry. -A `JsonSchema` instance will automatically register itself with the local registry upon calling `Evaluate()`. However, there are some cases where this may be insufficient. For example, in cases where schemas are separated across multiple files, it is necessary to register the schema instances prior to evaluation. +A `JsonSchema` instance will automatically register itself with the local registry during the build step. Generally, build order is important. You want to build dependencies first. For example, given these two schemas @@ -540,47 +477,32 @@ For example, given these two schemas } ``` -Here's the schema with an inline declaration: - -```c# -var schema = new JsonSchemaBuilder() - .Id("http://localhost/my-schema") - .Type(SchemaValueType.Object) - .Properties(("refProp", new JsonSchemaBuilder().Ref("http://localhost/random-string"))) - .Build(); -``` - -You must register `random-string` before you attempt to evaluate with `my-schema`. +You must build `random-string` before you build `my-schema`. ```c# var randomString = JsonSchema.FromFile("random-string.json"); -SchemaRegistry.Global.Register(new Uri("http://localhost/random-string"), randomString); +var mySchema = JsonSchema.FromFile("my-schema.json"); ``` Now _JsonSchema.Net_ will be able to resolve the reference. -> `JsonSchema.FromFile()` automatically sets the schema's base URI to the file path. If you intend to use file paths in your references (e.g. `file:///C:\random-string.json`), then just register the schema without passing a URI: -> -> ```c# -> SchemaRegistry.Global.Register(randomString); -> ``` -{: .prompt-warning} +_JsonSchema.Net_ will automatically handle reference loops, where one schema references another in such a way that some later reference eventually references the first. ## Resolving embedded schemas {#schema-embedded-schemas} In addition to schemas, other identifiable documents can be registered. For example, Open API documents _contain_ schemas but are not themselves schemas. Additionally, references between schemas within these documents are relative to the document root. Registering the Open API document will allow these references to be resolved. -A type may be registered if it implements `IBaseDocument`. For convenience, `JsonNodeBaseDocument` is included to support general JSON data. +A type may be registered if it implements `IBaseDocument`. For convenience, `JsonElementBaseDocument` is included to support general JSON data. -To create referenceable JSON data, simply create a `JsonNodeBaseDocument` wrapper for it and pass the data along with the URI that will be used to identify it. +To create referenceable JSON data, simply create a `JsonElementBaseDocument` wrapper for it and pass the data along with the URI that will be used to identify it. ```c# -var json = JsonNode.Parse(@"{ +var json = JsonDocument.Parse(@"{ ""foo"": 42, ""schema"": { ""type"": ""string"" } -}"); +}").RootElement; -var referenceableJson = new JsonNodeBaseDocument(json, "http://localhost/jsondata"); +var referenceableJson = new JsonElementBaseDocument(json, "http://localhost/jsondata"); SchemaRegistry.Global.Register(referenceableJson); var schema = new JsonSchemaBuilder() @@ -592,11 +514,11 @@ With the JSON document registered, the reference can resolve properly. ## Automatic resolution {#schema-ref-fetch} -In order to support scenarios where schemas cannot be registered ahead of time, the `SchemaRegistry` class exposes the `Fetch` property which is defined as `Func`. This property can be set to a method which downloads the content from the supplied URI and deserializes it into a `JsonSchema` object. +In order to support scenarios where schemas cannot be registered ahead of time, the `SchemaRegistry` class exposes the `Fetch` property which is defined as `Func`. This property can be set to a method which downloads the content from the supplied URI and deserializes it into an `IBaseDocument` object. The URI that is passed may need to be transformed, based on the schemas you're dealing with. For instance if you're loading schemas from a local filesystem, and the schema `$ref`s use relative paths, you may need to prepend the working folder to the URI in order to locate it. -## Bundling + -## Customizing error messages {#schema-errors} +# Customizing error messages {#schema-errors} The library exposes the `ErrorMessages` static type which includes read/write properties for all of the error messages. Customization of error messages can be achieved by setting these properties. -### Templates {#schema-error-templates} +## Templates {#schema-error-templates} Most of the error messages support token replacement. Tokens will use the format `[[foo]]` and will be replaced by the JSON serialization of the associated value. @@ -684,7 +606,7 @@ In this case, `[[received]]` will be replaced by the value in the JSON instance, > Since this example uses numbers, they appear without any particular formatting as this is how numbers serialize into JSON. Similarly, strings will render surrounded by double quotes, `true`, `false`, and `null` will appear using those literals, and more complex values like object and arrays will be rendered in their JSON representation. {: .prompt-info } -### Localization {#schema-error-localization} +## Localization {#schema-error-localization} In addition to customization, using resource files enables support for localization. The default locale is determined by `CultureInfo.CurrentCulture` and can be overridden by setting the `ErrorMessages.Culture` static property. diff --git a/_docs/schema/close.md b/_docs/schema/close.md index 921c6d0f..94c2b35f 100644 --- a/_docs/schema/close.md +++ b/_docs/schema/close.md @@ -2,5 +2,5 @@ title: __close permalink: /schema/:title/ close: true -order: "01.9" +order: "01.10" --- diff --git a/_docs/schema/codegen/close.md b/_docs/schema/codegen/close.md index a177b5c5..b0e85d51 100644 --- a/_docs/schema/codegen/close.md +++ b/_docs/schema/codegen/close.md @@ -2,5 +2,5 @@ title: __close close: true permalink: /schema/codegen/:title/ -order: "01.6.9" +order: "01.07.9" --- diff --git a/_docs/schema/codegen/mini-meta-schemas.md b/_docs/schema/codegen/mini-meta-schemas.md index 67f2665d..e76d6b24 100644 --- a/_docs/schema/codegen/mini-meta-schemas.md +++ b/_docs/schema/codegen/mini-meta-schemas.md @@ -3,7 +3,7 @@ layout: page title: Mini-Meta-Schema Reference permalink: /schema/codegen/:title/ icon: fas fa-tag -order: "01.6.3" +order: "01.07.3" --- > **DEPRECATION NOTICE** > diff --git a/_docs/schema/codegen/patterns.md b/_docs/schema/codegen/patterns.md index 97af82c3..9de542f8 100644 --- a/_docs/schema/codegen/patterns.md +++ b/_docs/schema/codegen/patterns.md @@ -3,7 +3,7 @@ layout: page title: Supported Patterns permalink: /schema/codegen/:title/ icon: fas fa-tag -order: "01.6.2" +order: "01.07.2" --- > **DEPRECATION NOTICE** > diff --git a/_docs/schema/codegen/schema-codegen.md b/_docs/schema/codegen/schema-codegen.md index 4224afc7..c55292a8 100644 --- a/_docs/schema/codegen/schema-codegen.md +++ b/_docs/schema/codegen/schema-codegen.md @@ -4,7 +4,7 @@ title: Generating Code from JSON Schema bookmark: Basics permalink: /schema/codegen/:title/ icon: fas fa-tag -order: "01.6.1" +order: "01.07.1" --- > **DEPRECATION NOTICE** > diff --git a/_docs/schema/codegen/title.md b/_docs/schema/codegen/title.md index cf3c4bca..ce863157 100644 --- a/_docs/schema/codegen/title.md +++ b/_docs/schema/codegen/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Code Generation permalink: /schema/codegen/:title/ folder: true -order: "01.6" +order: "01.07" --- diff --git a/_docs/schema/custom-keywords.md b/_docs/schema/custom-keywords.md new file mode 100644 index 00000000..fc92fa56 --- /dev/null +++ b/_docs/schema/custom-keywords.md @@ -0,0 +1,83 @@ +--- +layout: page +title: Defining and Using Custom Keywords +bookmark: Custom Keywords +permalink: /schema/:title/ +icon: fas fa-tag +order: "01.03" +--- +_JsonSchema.Net_ has been designed with custom keywords in mind. Using custom keywords just take two steps: + +1. Implement `IKeywordHandler`. +2. Create a dialect. + +Lastly, remember that the best resource for building keywords is [the code](https://github.com/gregsdennis/json-everything/tree/master/JsonSchema/Keywords) where all of the built-in keywords are defined. + +### Evaluation philosophy + +Starting with version 8 of _JsonSchema.Net_, schema evaluation occurs in two stages: building a cyclical graph of subschema nodes and processing evaluations. The build stage performs any work that can be done without an instance, while evaluations complete the work. By separating these stages, _JsonSchema.Net_ can reuse work for many evaluations, greatly improving performance. + +### 1. Implement `IKeywordHandler` {#schema-custom-keywords-1} + +Implementing your keyword will require some initial thought and design around what work the keyword can perform without the instance and what work requires the instance. + +> The keywords that ship with the library have been created as singletons. Though not required, this is a recommended practice. +{: .prompt-tip } + +The interface defines a property and three methods: + +#### `Name` + +This is just the name of the keyword. + +#### `object? ValidateKeywordValue(JsonElement)` + +This method validates that the keyword's value in the schema is correct. It is expected that this method will throw `JsonSchemaException` if the value is invalid for the keyword. + +Optionally, this method can return a value that can be used in other methods. For example, the `$ref` keyword returns the URI value. Although the `KeywordData` does retain the raw `JsonElement`, the URI has already been parsed by this method, and we can save a bit of ticks and memory by simply reusing it instead of having to parse it again. + +#### `void BuildSubschemas(KeywordData, BuildContext)` + +This method builds subschemas and add them onto the keyword data. + +In building the subschemas, this method is also responsible for creating new build context structs with updated details like the instance, its location, and the relative path from its parent (less the keyword itself). Mostly these details are needed for consistent output values. + +#### `EvaluationResults Evaluate(KeywordData, EvaluationContext)` + +This method actually performs the evaluation. + +At this point, usually all of the pieces are in place and you just have to do the check. + +#### Builder extensions {#schema-builder-extensions} + +To enable the fluent construction interface for your keyword, simply create an extension method on `JsonSchemaBuilder` that adds the keyword and returns the builder. For example, adding a `description` keyword is implemented by this method: + +```c# +public static JsonSchemaBuilder Description(this JsonSchemaBuilder builder, string description) +{ + builder.Add("description", description); + return builder; +} +``` + +### 2. Create a dialect {#schema-custom-keywords-2} + +To make *JsonSchema.Net* aware of your keyword, you must create a new dialect that contains it. + +```c# +var myDialect = Dialect.Draft202012.With([Mykeyword.Instance]); + +var buildOptions = new BuildOptions +{ + Dialect = myDialect +} +``` + +If you have an ID for your dialect, and you want to allow schemas to declare it using the `$schema` keyword, you'll need to + +- create a meta-schema and add it to the schema registry +- add your dialect to the dialect registry + +## You're done + +That's it, really. Your keyword is ready to use. Just assign your new dialect to the build options, and it'll be handled. \ No newline at end of file diff --git a/_docs/schema/datagen/close.md b/_docs/schema/datagen/close.md index e6dffdd7..41758453 100644 --- a/_docs/schema/datagen/close.md +++ b/_docs/schema/datagen/close.md @@ -2,5 +2,5 @@ title: __close close: true permalink: /schema/datagen/:title/ -order: "01.7.9" +order: "01.06.9" --- diff --git a/_docs/schema/datagen/schema-datagen.md b/_docs/schema/datagen/schema-datagen.md index 6ac989a1..8e1657ed 100644 --- a/_docs/schema/datagen/schema-datagen.md +++ b/_docs/schema/datagen/schema-datagen.md @@ -4,7 +4,7 @@ title: Generating Sample JSON Data from a Schema bookmark: Basics permalink: /schema/datagen/:title/ icon: fas fa-tag -order: "01.7.1" +order: "01.06.1" --- *JsonSchema.Net.DataGeneration* is a tool that can create JSON data instances using a JSON schema as a framework. diff --git a/_docs/schema/datagen/title.md b/_docs/schema/datagen/title.md index 43d62c39..9a46af67 100644 --- a/_docs/schema/datagen/title.md +++ b/_docs/schema/datagen/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Data Generation permalink: /schema/datagen/:title/ folder: true -order: "01.7" +order: "01.06" --- diff --git a/_docs/schema/examples/close.md b/_docs/schema/examples/close.md index 0e352dd2..e859a7cc 100644 --- a/_docs/schema/examples/close.md +++ b/_docs/schema/examples/close.md @@ -2,5 +2,5 @@ title: __close close: true permalink: /schema/examples/:title/ -order: "01.4.9" +order: "01.05.9" --- diff --git a/_docs/schema/examples/custom-keywords.md b/_docs/schema/examples/custom-keywords.md new file mode 100644 index 00000000..2c085615 --- /dev/null +++ b/_docs/schema/examples/custom-keywords.md @@ -0,0 +1,110 @@ +--- +layout: page +title: Example - Extending JSON Schema Validation With Your Own Keywords +bookmark: Custom Keywords +permalink: /schema/examples/:title/ +icon: fas fa-tag +order: "01.05.4" +--- +These examples will show how to extend JSON Schema validation by creating a new keyword and incorporating it into a new vocabulary. + +> These examples are actually defined in one of the library's unit tests. +{: .prompt-info } + +For a more detailed explanation about the concepts behind vocabularies, please see the Vocabularies page. + +## Defining a keyword + +We want to define a new `maxDate` keyword that allows a schema to enforce a maximum date value to appear in an instance property. We'll start with the keyword. + +```c# +public class MaxDateKeyword : IKeywordHandler +{ + public static MaxDateKeyword Instance { get; set; } = new(); + + public string Name => "maxDate"; + + private MaxDateKeyword(){} + + public object? ValidateKeywordValue(JsonElement value) + { + if (value.ValueKind is not JsonValueKind.String) + throw new JsonSchemaException($"'{Name}' value must be a string, found {value.ValueKind}"); + + return DateTime.Parse(value.GetString()!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + } + + public void BuildSubschemas(KeywordData keyword, BuildContext context) + { + } + + public KeywordEvaluation Evaluate(KeywordData keyword, EvaluationContext context) + { + if (context.Instance.ValueKind is not JsonValueKind.String) return KeywordEvaluation.Ignore; + + var dateString = context.Instance.GetString(); + var date = DateTime.Parse(dateString!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + var expectedDate = (DateTime)keyword.Value!; + + if (date > expectedDate) + { + return new KeywordEvaluation + { + Keyword = Name, + IsValid = false, + Error = $"Date must be on or before {expectedDate:O}" + }; + } + + return new KeywordEvaluation + { + Keyword = Name, + IsValid = true + }; + } +} +``` + +> Note that the keyword is a singleton. All of the built-in keywords are stateless and implemented as singletons. Though not a requirement, this greatly reduces the footprint of the runtime. +{: .prompt-tip } + +## Defining a dialect + +Now that we have the keyword, we need to tell the system about it. The easiest way to do this is to create a copy of an existing dialect and add your keyword. + +```c# +var myDialect = Dialect.202012.With([MaxDateKeyword.Instance]); +``` + +This custom dialect can now be included on your build options when building your schema. + +```c# +var options = new BuildOptions +{ + Dialect = myDialect +} +``` + +If you want to make your dialect generally available and identifiable from the `$schema` keyword, there's a bit more to do. + +First, you'll want to create a meta-schema. For our single keyword, you can do something like this: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mycompany.org/schemas/dialects/my-dialect", + "$ref": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "maxDate": { + "type": "string", + "format": "date-time" + } + } +} +``` + +Build this meta-schema (which automatically registers it). + +Next, register your new dialect in the registry. You may want to maintain a dialect registry, or you can just use the global one, which is the default on a `BuildOptions` object. + +And that's it. Your dialect is now available for use through that build options object. Again, you can use the default build options if you want it to be available everywhere. \ No newline at end of file diff --git a/_docs/schema/examples/custom-vocabs.md b/_docs/schema/examples/custom-vocabs.md deleted file mode 100644 index 6485ba04..00000000 --- a/_docs/schema/examples/custom-vocabs.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -layout: page -title: Example - Extending JSON Schema Validation With Your Own Vocabularies -bookmark: Custom Vocabs -permalink: /schema/examples/:title/ -icon: fas fa-tag -order: "01.4.4" ---- -These examples will show how to extend JSON Schema validation by creating a new keyword and incorporating it into a new vocabulary. - -> These examples are actually defined in one of the library's unit tests. -{: .prompt-info } - -For a more detailed explanation about the concepts behind vocabularies, please see the Vocabularies page. - -## Defining a Keyword {#example-schema-vocabs-keyword} - -We want to define a new `maxDate` keyword that allows a schema to enforce a maximum date value to appear in an instance property. We'll start with the keyword. - -```c# -// The SchemaKeyword attribute is how the deserializer knows to use this -// class for the "maxDate" keyword. -[SchemaKeyword(Name)] -// Naturally, we want to be able to deserialize it. -[JsonConverter(typeof(MaxDateJsonConverter))] -// We need to declare which vocabulary this keyword belongs to. -[Vocabulary("http://mydates.com/vocabulary")] -// Specify which versions the keyword is compatible with. -[SchemaSpecVersion(SpecVersion.Draft201909 | SpecVersion.Draft202012)] -public class MaxDateKeyword : IJsonSchemaKeyword -{ - // Define the keyword in one place. - public const string Name = "maxDate"; - - // Define whatever data the keyword needs. - public DateTime Date { get; } - - public MaxDateKeyword(DateTime date) - { - Date = date; - } - - // Implements IJsonSchemaKeyword - public KeywordConstraint GetConstraint(SchemaConstraint schemaConstraint, - ReadOnlySpan localConstraints, - EvaluationContext context) - { - throw new NotImplementedException(); - } -} -``` - -We need to define that serializer, too. - -```c# -class MaxDateJsonConverter : JsonConverter -{ - public override MaxDateKeyword Read(ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - { - // Check to see if it's a string first. - if (reader.TokenType != JsonTokenType.String) - throw new JsonException("Expected string"); - - var dateString = reader.GetString(); - // If the parse fails, then it's not in the right format, - // and we should throw an exception anyway. - var date = DateTime.Parse(dateString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); - - return new MaxDateKeyword(date); - } - - public override void Write(Utf8JsonWriter writer, - MaxDateKeyword value, - JsonSerializerOptions options) - { - writer.WriteStringValue(value.Date.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssK")); - } -} -``` - -## Registering the Keyword {#example-schema-vocabs-register-keyword} - -If you're keen on creating a vocabulary, go on to the next section. Otherwise, keep reading. - -Now that we have the keyword, we need to tell the system about it. - -```c# -SchemaKeywordRegistry.Register(); -``` - -> If your app is running in a Native AOT context, you'll need to use the `.Register()` overload to properly support serialization. -{: .prompt-info } - -> If you're building a dynamic system where you don't always want the keyword supported, it can be removed using the `SchemaKeywordRegistry.Unregister()` static method. -{: .prompt-tip } - -You'll also want to set the `EvaluationOptions.ProcessCustomKeywords` option to true so that non-dialect keywords are processed. - -That's technically all you need to do to support a custom keyword. However, going forward for JSON Schema, custom keywords should be defined in a custom vocabulary. - -## Defining a Vocabulary {#example-schema-vocabs-definition} - -Vocabularies are used within JSON Schema to ensure that the validator you're using supports your new keyword. Because we have already created the keyword and registered it, we know it is supported. - -However, we might not be implementing _our_ vocabulary. This keyword is likely from a third party who has written a schema that declares a vocabulary that defines `maxDate`, and we're trying to support _that_. - -In accordance with the specification, JsonSchema.Net will refuse to process any schema whose meta-schema declares a vocabulary it doesn't know about. Because of this, it won't process the third-party schema unless we define the vocabulary on our end. - -```c# -static class MyCustomVocabularies -{ - // Define the vocabulary and list the keyword types it defines. - public static readonly Vocabulary DatesVocabulary = - new Vocabulary("http://mydates.com/vocabulary", typeof(MaxDateKeyword)); - - // Although not required a vocabulary may also define a vocab meta-schema. - // It's a good idea to implement that as well. - // This meta-schema only needs to validate that keyword syntax is correct. - public static readonly JsonSchema DatesMetaSchema = - new JsonSchemaBuilder() - .Id("http://mydates.com/vocab/schema") - .Schema(MetaSchemas.Draft202012Id) - .Properties( - (MaxDateKeyword.Name, new JsonSchemaBuilder() - .Type(SchemaValueType.String) - .Format(Formats.DateTime) - ) - ); - - // You'll also want to define a new dialect, usually based on an existing - // dialect. This one is based on the 2020-12 dialect. - // Splitting the vocab and dialect meta-schemas this way makes it easier to - // re-use the vocabulary across multiple dialects. - public static readonly JsonSchema DatesDialectMetaSchema = - new JsonSchemaBuilder() - .Id("http://mydates.com/dialect/schema") - .Schema(MetaSchemas.Draft202012Id) - .Vocabulary( - // You have to list all of the vocabularies you plan on using. - (Vocabularies.Core202012Id, true), - (Vocabularies.Applicator202012Id, true), - (Vocabularies.Unevaluated202012Id, true), - (Vocabularies.Validation202012Id, true), - (Vocabularies.Metadata202012Id, true), - (Vocabularies.FormatAnnotation202012Id, true), - (Vocabularies.Content202012Id, true), - // Don't forget to list your vocab. - ("http://mydates.com/vocabulary", true) - ) - // Now we need to reference the 2020-12 meta-schema and our - // vocab meta-schema so that all the keywords are validated. - .AllOf( - new JsonSchemaBuilder().Ref(Vocabularies.Core202012Id), - new JsonSchemaBuilder().Ref("http://mydates.com/vocab/schema") - ); -} -``` - -Then they need to be registered. This is done on the schema validation options. - -```c# -// Register both meta-schemas. -options.SchemaRegistry.Register(new Uri("http://mydates.com/vocab/schema"), DatesMetaSchema); -options.SchemaRegistry.Register(new Uri("http://mydates.com/dialect/schema"), DatesDialectMetaSchema); - -// Register the vocabulary. -// You'll still need to register the keywords as shown in the previous section. -VocabularyRegistry.Register(DatesVocabulary); -``` - -And that's it. The vocabulary and keyword are ready for use. diff --git a/_docs/schema/examples/external-schemas.md b/_docs/schema/examples/external-schemas.md index 09d87641..9f5ed07b 100644 --- a/_docs/schema/examples/external-schemas.md +++ b/_docs/schema/examples/external-schemas.md @@ -4,7 +4,7 @@ title: Example - Handling References to External Schemas bookmark: External References permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.4.2" +order: "01.05.2" --- JSON Schema has multiple ways to reference other schemas. This is done to both reduce the size of the schemas that we humans have to deal with as well as to promote code reuse. Defining a schema once to be used in multiple places is often a better approach than rewriting it in all of those places. It also allows us to define recursive schemas. @@ -17,7 +17,7 @@ References typically come in two flavors: internal and external. Internal refer *JsonSchema.Net* will automatically handle internal references. The schema document is loaded, and the library can easily resolve pointers inside of it. -In order for *JsonSchema.Net* to handle external schemas, however, the schemas must be loaded and registered before validation starts. +In order for *JsonSchema.Net* to handle external schemas, however, the external schemas must be built (which registers them) before the schemas that reference them. Suppose you have a subfolder where you store your schema files. To load them, just iterate through the files and register them with `SchemaRegistry`. @@ -25,20 +25,44 @@ Suppose you have a subfolder where you store your schema files. To load them, j var files = Directory.GetFiles("my-schemas", "*.json"); foreach (var file in files) { + // automatically adds to the global registry var schema = JsonSchema.FromFile(file); - SchemaRegistry.Global.Register(schema); } ``` It's best practice to ensure all of your schemas declare an `$id` keyword at their root. If a schema doesn't have this keyword, `FromFile()` will automatically assign the `file:///` URI of the full file name in order to reference this schema. -`SchemaRegistry.Global.Register()` is the part that matters here. This adds the schema to the internal registry so that, when the schema is needed, it can be found. +### Reference loops + +Sometimes, you may have a reference loop where one schema references another which references back to the first. As long as this does result in an infinite cycle, you're good (the specifications require implementations to detect and reject such loops). As an extremely contrived example, consider these schemas which describe a linked list of alternating integers and strings: + +```json +{ + "$id": "integer-node", + "type": "object", + "properties": { + "value": { "type": "integer" }, + "next": { "$ref": "string-node" } + } +} + +{ + "$id": "string-node", + "type": "object", + "properties": { + "value": { "type": "string" }, + "next": { "$ref": "integer-node" } + } +} +``` + +You can see that it's impossible to determine a "correct" build order because they depend on each other. To support this, _JsonSchema.Net_ will leave references unresolved while building `integer-node` without throwing a `RefResolutionException`. When `string-node` is then built, it will propertly resolve `integer-node` and then attempt to also resolve its references, thereby closing the loop. ## Dynamically loading references {#example-schema-fetching} -An alternative to preloading schemas is setting up an automatic download by setting the `SchemaRegistry.Global.Fetch` function property. +An alternative to preloading schemas is setting up an automatic download by setting the `Fetch` function property of the schema registry. -> Automatically downloading external data is [explicitly recommended against](https://json-schema.org/draft/2020-12/json-schema-core.html#name-schema-references) by the specification. This functionality is added for convenience and disabled by default. +> Automatically downloading external data is [explicitly discouraged](https://json-schema.org/draft/2020-12/json-schema-core.html#name-schema-references) by the specification. This functionality is added for convenience and disabled by default. {: .prompt-warning } ```c# @@ -58,7 +82,7 @@ JsonSchema? DownloadSchema(Uri uri) SchemaRegistry.Global.Fetch = DownloadSchema; ``` -To clear the download function, simply set `null`. The property isn't declared as nullable, but this will reset the property to a function that just returns null. +To clear the download function, simply set `null`. The property isn't declared as nullable, but this will reset the property to a no-op function. ```c# SchemaRegistry.Global.Fetch = null!; diff --git a/_docs/schema/examples/legacy-output.md b/_docs/schema/examples/legacy-output.md deleted file mode 100644 index f047571d..00000000 --- a/_docs/schema/examples/legacy-output.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: page -title: Example - Serializing Output in the 2019-09 / 2020-12 Format -bookmark: Legacy Output -permalink: /schema/examples/:title/ -icon: fas fa-tag -order: "01.4.5" ---- -> **DEPRECATION NOTICE** -> -> The `Pre202012EvaluationResultsJsonConverter` class has been marked obsolete and will be removed in the next major version. -{: .prompt-danger } - -The 2019-09 and 2020-12 JSON Schema specifications define output formats that can be difficult to work with. - -For future versions of the specification, the output is undergoing some [changes](https://json-schema.org/blog/posts/fixing-json-schema-output). This new format is the default for _JsonSchema.Net_, however the evaluation results can still (mostly) be serialized to the 2019-09 / 2020-12 formats by using the `Pre202012EvaluationResultsJsonConverter`. - -```c# -var schema = JsonSchema.FromText(" ... "); -var options = new EvaluationOptions -{ - OutputFormat = OutputFormats.List -}; -var instance = JsonNode.Parse(" ... "); -var results = schema.Evaluate(instance, options); - -var serializerOptions = new JsonSerializerOptions -{ - Converters = { new Pre202012EvaluationResultsJsonConverter() } -}; -var output = JsonSerializer.Serialize(results, serializationOptions); -``` - -The `OutputFormat` option still controls which format you'll get: - -| `OutputFormat` | "New" format (default) | "Legacy" format | -|:-:|:-:|:-:| -| `Flag` | flag | flag | -| `List` | list | basic | -| `Hierarchical` | hierarchical | verbose | - -> The "detailed" format defined by the specification is not supported since it requires some advanced logic (which was never properly specified) to pare down the nodes. -{: .prompt-warning} diff --git a/_docs/schema/examples/managing-options.md b/_docs/schema/examples/managing-options.md deleted file mode 100644 index f8f37657..00000000 --- a/_docs/schema/examples/managing-options.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -layout: page -title: Example - Configuring Schema Evaluation -bookmark: Configuration -permalink: /schema/examples/:title/ -icon: fas fa-tag -order: "01.4.1" ---- -There are a few objects which declare static default values: - -- `EvaluationOptions` -- `SchemaRegistry` -- `VocabularyRegistry` - -All of these objects are also instance objects which can be configured independently of the defaults. When schema evaluation begins, it makes copies of everything and runs from the copies. This means that every evaluation run can be completely isolated if needed. - -> `SchemaRegistry` and `VocabularyRegistry` are available as properties on the `EvaluationOptions`. -{: .prompt-info } - -Additionally, for the two registries, the default instances are used as fallbacks for when the requested value can't be found in the local instance. - -Let's look at this a bit more deeply. - -## Setting output format {#example-schema-options-output} - -To set `Detailed` as the default output format for _all_ evaluations, use the default instance: - -```c# -EvaluationOptions.Default.OutputFormat = OutputFormat.Detailed; -``` - -However if you want to then get a `Verbose` output for just _one_ evaluation run, you can create a new options object and pass this into the `.Evaluate()` call. - -```c# -var options = new EvaluationOptions { OutputFormat = OutputFormat.Verbose }; -var results = schema.Evaluate(instance, options); -``` - -When you create a new options object, it copies all of the values from the default instance (`EvaluationOptions.Default`) as a starting point. From there, you can make changes to your instance without affecting other evaluations. - -`SchemaRegistry` and `VocabularyRegistry` options work a bit differently, though. - -## Configuring SchemaRegistry and VocabularyRegistry {#example-schema-options-registries} - -The default instance for these objects is actually called `Global` because they serve as a fallback for when the local instances can't find what's being requested. Let's look at preloading schemas to see how the schema vocabulary manages a local registry against the global one. - -> `VocabularyRegistry` works exactly the same. -{: .prompt-tip } - -As mentioned in the [Handling Externally-defined Schemas](#handling-externally-defined-schemas) example, to reference external schemas, they need to be preloaded first. That example shows simply adding the schemas to the global registry. This makes the schema available to _all_ evaluations. - -```c# -SchemaRegistry.Global.Register(schema); -``` - -When the evaluation runs, a new, empty registry is created and set onto the options. When the evaluator encounters a `$ref` keyword with a URI for an external document, it calls `options.SchemaRegistry.Get()` to retrieve the referenced schema. Note that this is the options object's local copy. Since you haven't set anything there, nothing is found. So it then checks the global registry and finds it. - -It's doing this for you: - -```c# -return localRegistry.Get(uri) ?? globalRegistry.Get(uri); -``` - -As mentioned, registering the schema with the global registry makes the schema available to everyone because of this fallback logic. To only make the schema available to a single evaluation, you'll need to register with the local registry. This must be done through the options object prior to evaluation. - -```c# -var options = new EvaluationOptions(); -options.SchemaRegistry.Register(externalSchema); - -var results = schema.Evaluate(instance, options); -``` - -Now, the local registry will find something, and it won't fall back to the global. Moreover, your external schema is only available for this evaluation (or any evaluation that uses this options object). diff --git a/_docs/schema/examples/title.md b/_docs/schema/examples/title.md index b7d845e8..ff625242 100644 --- a/_docs/schema/examples/title.md +++ b/_docs/schema/examples/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Examples folder: true permalink: /schema/examples/:title/ -order: "01.4" +order: "01.05" --- diff --git a/_docs/schema/examples/version-selection.md b/_docs/schema/examples/version-selection.md index 25565b14..d416f9a4 100644 --- a/_docs/schema/examples/version-selection.md +++ b/_docs/schema/examples/version-selection.md @@ -4,49 +4,48 @@ title: Example - JSON Schema Specification Version Selection bookmark: Schema Version permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.4.3" +order: "01.05.3" --- -Selecting the right JSON Schema version (historically known as "draft") can be an important factor in ensuring evaluation works as expected. Selecting the wrong draft may result in some keywords not being processed. For example, `prefixItems` was only added with draft 2020-12. Evaluating a schema with this keyword under a previous draft will ignore the keyword completely. +Selecting the right JSON Schema version (historically known as "draft") can be an important factor in ensuring evaluation works as expected. Selecting the wrong draft may result in some keywords not being processed. For example, `prefixItems` was only added with Draft 2020-12. Evaluating a schema with this keyword under a previous draft will ignore the keyword completely. *JsonSchema.Net* has a couple ways to specify which version a evaluation should use. ## `$schema` -Including this keyword in the schema itself is the preferred way to specify which version to use when interpreting a schema. The specification itself _strongly recommends_ that all schemas contain this keyword. +Including this keyword in the schema is the preferred way to specify which version to use when interpreting a schema. The specification itself _strongly recommends_ that all schemas contain this keyword. If you're the author of your schemas, just include this keyword, and all will be well. *JsonSchema.Net* will always respect this keyword when present. -## Evaluation Options {#example-schema-versions-options} +## Build Options {#example-schema-versions-options} -If the schema you're working with is out of your control, meaning you can't add a `$schema` keyword, there is some logic to determine the best candidate automatically, however the `EvaluationOptions.EvaluateAs` property will be your friend. - -This option allows you to specify which draft you want to use during evaluation. - -By default, the latest supported version will be used. +If the schema you're working with is out of your control, meaning you can't add a `$schema` keyword, you can specify which draft you want to use during evaluation using the `BuildOptions.Dialect` property. > The value for `$schema` is a URI (identifier), not a URL (location). This means that the value must exactly match the `$id` from a known meta-schema. This also means there is no `https`-for-`http` substitution. {: .prompt-warning } +By default, the latest supported version will be used. At the time of this writing, that is the preview v1/2026. + # Examples {#example-schema-versions-examples} ## Behaviors of Explicitly Specifying Different Versions {#example-schema-versions-explicit} ```c# -JsonSchema schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .PrefixItems( - new JsonSchemaBuilder() - .Type(SchemaValueType.Integer), - new JsonSchemaBuilder() - .Type(SchemaValueType.Boolean) - ) - .Items(new JsonSchemaBuilder() - .Type(SchemaValueType.String) - ) - -var instance = new JsonArray { 1, true, "foo", "bar" }; +var schemaJson = JsonDocument.Parse( + """ + { + "type": "array", + "prefixItems": [ + { "type": "integer" }, + { "type": "boolean" } + ], + "items": { "type": "string" } + } + """).RootElement; +JsonSchema schema = JsonSchema.Build(schemaJson); + +var instance = JsonDocument.Parse("""[1, true, "foo", "bar"]""").RootElement; ``` This builds a schema that evaluates JSON instances which are arrays with: diff --git a/_docs/schema/schemagen/close.md b/_docs/schema/schemagen/close.md index aed364ce..593e780b 100644 --- a/_docs/schema/schemagen/close.md +++ b/_docs/schema/schemagen/close.md @@ -2,5 +2,5 @@ title: __close permalink: /schema/schemagen/:title/ close: true -order: "01.5.9" +order: "01.06.9" --- diff --git a/_docs/schema/schemagen/conditional-generation.md b/_docs/schema/schemagen/conditional-generation.md index f24fbe7e..b072bb3d 100644 --- a/_docs/schema/schemagen/conditional-generation.md +++ b/_docs/schema/schemagen/conditional-generation.md @@ -4,7 +4,7 @@ title: Conditional JSON Schema Generation bookmark: Conditionals permalink: /schema/schemagen/:title/ icon: fas fa-tag -order: "01.5.2" +order: "01.06.2" --- Draft 7 of JSON Schema introduced a nice way to include some conditional constraints into your schemas. The most common way that people use these is to apply different constraints to various properties based on the value of another property. This is similar to the `discriminator` keyword offered by Open API. diff --git a/_docs/schema/schemagen/data-annotations.md b/_docs/schema/schemagen/data-annotations.md index 79010828..9d16559a 100644 --- a/_docs/schema/schemagen/data-annotations.md +++ b/_docs/schema/schemagen/data-annotations.md @@ -4,7 +4,7 @@ title: JSON Schema Generation with System.ComponentModel.DataAnnotations bookmark: Data Annotations permalink: /schema/schemagen/:title/ icon: fas fa-tag -order: "01.5.3" +order: "01.06.3" --- The _System.ComponentModel.DataAnnotations_ namespace defines numerous attributes that are commonly used within ASP.Net and other areas to validate data models. diff --git a/_docs/schema/schemagen/examples/attribute.md b/_docs/schema/schemagen/examples/attribute.md index 38b53d2f..644d0a9f 100644 --- a/_docs/schema/schemagen/examples/attribute.md +++ b/_docs/schema/schemagen/examples/attribute.md @@ -4,7 +4,7 @@ title: Using Attributes to Add Constraints bookmark: Attributes permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.5.4.3" +order: "01.06.4.3" --- In the [previous example](/schema/examples/intent/) we created a keyword intent to represent the [`maxDate` keyword](/schema/examples/custom-vocabs/#example-schema-vocabs-keyword) during generation. diff --git a/_docs/schema/schemagen/examples/close.md b/_docs/schema/schemagen/examples/close.md index b5bfa37b..87b9cc45 100644 --- a/_docs/schema/schemagen/examples/close.md +++ b/_docs/schema/schemagen/examples/close.md @@ -2,5 +2,5 @@ title: __close permalink: /schema/schemagen/examples/:title/ close: true -order: "01.5.4.9" +order: "01.06.4.9" --- diff --git a/_docs/schema/schemagen/examples/generator.md b/_docs/schema/schemagen/examples/generator.md index de7dd61b..04c3ed53 100644 --- a/_docs/schema/schemagen/examples/generator.md +++ b/_docs/schema/schemagen/examples/generator.md @@ -4,7 +4,7 @@ title: Generating a Schema for a Simple Type bookmark: Generators permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.5.4.1" +order: "01.06.4.1" --- This example shows how to extend schema generation to cover a specific type that isn't defined by the type's properties. This is useful for many of the scalar-like value types, such as `bool`, `int`, `DateTime`, or `TimeSpan`. diff --git a/_docs/schema/schemagen/examples/intent.md b/_docs/schema/schemagen/examples/intent.md index 2e5edf65..5727d5d7 100644 --- a/_docs/schema/schemagen/examples/intent.md +++ b/_docs/schema/schemagen/examples/intent.md @@ -4,7 +4,7 @@ title: Supporting a New Keyword During Generation bookmark: Intents permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.5.4.2" +order: "01.06.4.2" --- This example shows how to extend schema generation to output a new keyword. diff --git a/_docs/schema/schemagen/examples/multiple-ifs-one-group.md b/_docs/schema/schemagen/examples/multiple-ifs-one-group.md index 9ce0a0aa..dbfaa59e 100644 --- a/_docs/schema/schemagen/examples/multiple-ifs-one-group.md +++ b/_docs/schema/schemagen/examples/multiple-ifs-one-group.md @@ -4,7 +4,7 @@ title: Using Multiple `[If]` Attributes in a Single Group bookmark: Stacking [If]s permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.5.4.5" +order: "01.06.4.5" --- This example shows how multiple `[If]` attributes can be combined to create "OR" logic. diff --git a/_docs/schema/schemagen/examples/refiner.md b/_docs/schema/schemagen/examples/refiner.md index 877905cb..d38a338d 100644 --- a/_docs/schema/schemagen/examples/refiner.md +++ b/_docs/schema/schemagen/examples/refiner.md @@ -4,7 +4,7 @@ title: Performing Custom Generation bookmark: Refiners permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.5.4.4" +order: "01.06.4.4" --- # Performing Custom Generation {#example-schemagen-refiner} diff --git a/_docs/schema/schemagen/examples/title.md b/_docs/schema/schemagen/examples/title.md index 28a5f721..2a989752 100644 --- a/_docs/schema/schemagen/examples/title.md +++ b/_docs/schema/schemagen/examples/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Examples permalink: /schema/schemagen/examples/:title/ folder: true -order: "01.5.4" +order: "01.06.4" --- diff --git a/_docs/schema/schemagen/schema-generation.md b/_docs/schema/schemagen/schema-generation.md index 2df03912..dae60411 100644 --- a/_docs/schema/schemagen/schema-generation.md +++ b/_docs/schema/schemagen/schema-generation.md @@ -4,7 +4,7 @@ title: Generating JSON Schema from .Net Types bookmark: Basics permalink: /schema/schemagen/:title/ icon: fas fa-tag -order: "01.5.1" +order: "01.06.1" --- _JsonSchema.Net.Generation_ is an extension package to _JsonSchema.Net_ that provides JSON Schema generation from .Net types. diff --git a/_docs/schema/schemagen/title.md b/_docs/schema/schemagen/title.md index 43ddd09d..5106e410 100644 --- a/_docs/schema/schemagen/title.md +++ b/_docs/schema/schemagen/title.md @@ -2,6 +2,6 @@ title: __title bookmark: Schema Generation folder: true -order: "01.5" +order: "01.06" permalink: /schema/schemagen/:title/ --- diff --git a/_docs/schema/serialization.md b/_docs/schema/serialization.md index 379d142b..f6b0d331 100644 --- a/_docs/schema/serialization.md +++ b/_docs/schema/serialization.md @@ -4,7 +4,7 @@ title: Enhancing Deserialization with JSON Schema bookmark: Serialization with Validation permalink: /schema/:title/ icon: fas fa-tag -order: "01.2" +order: "01.02" --- *JsonSchema.Net* includes a JSON converter implementation that provides JSON validation support _during_ deserialization. @@ -24,29 +24,35 @@ Let's walk through it. Custom JSON converters are added via the `JsonSerializationOptions.Converters` property. Any converters in this collection will have priority over the default set of converters that ship with .Net. You can read more about custom converters in their [documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0). -When preparing to deserialize your payload, create an options object and add the `ValidatingJsonConverter` from JsonSchema.Net: +When preparing to deserialize your payload, create an options object and add the `ValidatingJsonConverter` from _JsonSchema.Net_: ```c# var options = new JsonSerializationOptions { - Converters = { new ValidatingJsonConverter - { - Options = + Converters = { + new ValidatingJsonConverter { - // set evaluation options + EvaluationOptions = + { + // set evaluation options + } } } }; ``` -or +or the `GenerativeValidatingJsonConverter` from _JsonSchema.Net.Generation_: ```c# var options = new JsonSerializationOptions { - Converters = { new ValidatingJsonConverter + Converters = { new GenerativeValidatingJsonConverter { - Options = + BuildOptions = + { + // set build options + }, + EvaluationOptions = { // set evaluation options }, @@ -58,7 +64,7 @@ var options = new JsonSerializationOptions }; ``` -Whenever you deserialize with these options, this converter will be queried to see if the type to deserialize is configured with a `[JsonSchema()]` attribute. If it is, then the payload will be validated against the schema prior to deserialization. +Whenever you deserialize with these options, this converter will be queried to see if the type to deserialize is configured with a `[JsonSchema()]` attribute or a `[GenerateJsonSchema]` attribute. If it is, then the payload will be validated against the schema prior to deserialization. ```c# var myModel = JsonSerializer.Deserialize(jsonText, options); @@ -72,7 +78,8 @@ If the data isn't valid, then a `JsonException` will be thrown. The validation The validation can be configured using properties on the converter. -- `Options`, which is available on both converters, configures schema evaluations. +- `EvaluationOptions`, which is available on both converters, configures schema evaluations. +- `BuildOptions`, which is available on the generative version only, configures the schema build step. - `GeneratorConfiguration`, which is available on the generative version only, configures schema generation. > The `ValidatingJsonConverter` and `GenerativeValidatingJsonConverter` are factories that create individual typed converters and cache them. Be aware, however, that when a typed converter is used, its options are overwritten with the options you've set on the factory. This has a side effect of rendering the typed converter unsafe in multithreaded environments when using varying options. It'll be fine if you always use the same options, however. diff --git a/_docs/schema/vocabs.md b/_docs/schema/vocabs.md index db061f6a..a54cb859 100644 --- a/_docs/schema/vocabs.md +++ b/_docs/schema/vocabs.md @@ -4,13 +4,13 @@ title: Extending JSON Schema Using Vocabularies bookmark: Vocabularies permalink: /schema/:title/ icon: fas fa-tag -order: "01.3" +order: "01.04" --- -JSON Schema draft 2019-09 introduced the idea of vocabularies to enable some spec support for custom keywords. +JSON Schema drafts 2019-09 and 2020-12 define the idea of vocabularies to enable some spec support for custom keywords. A vocabulary is just a collection of keywords. It will be identified by a URI and should have an associated specification that describes the function of each of the keywords. There *may* also be an associated meta-schema. -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. +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 2020-12 schemas, it is strongly suggested. ## Defining a vocabulary @@ -103,240 +103,12 @@ Any validator can validate that `minDate` is a date-formatted string, but only a 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). -## Registering a vocabulary {#schema-vocabs-registration} +## Creating and registering a vocabulary {#schema-vocabs-registration} -To tell *JsonSchema.Net* about a vocabulary, you need to create a `Vocabulary` instance and register it using `VocabularyRegistry.Register()`. +To tell *JsonSchema.Net* about a vocabulary, you need to create a `Vocabulary` instance and register it using `VocabularyRegistry.Register()`. This can be registered with either the global registry or with the one on the build options. The `Vocabulary` class is quite simple. It defines the vocabulary's ID and lists the keywords which it supports. -The keywords must still be registered separately (keep reading for instructions on creating and registering keywords). +You will still need to create a custom dialect to make use of your keyword. -## Defining Custom Keywords {#schema-vocabs-custom-keywords} - -_JsonSchema.Net_ has been designed with custom keywords in mind. There are several steps that need to be performed to do this. - -1. Implement `IJsonSchemaKeyword`. -2. Optionally implement one of the schema-container interfaces. - 1. `ISchemaContainer` - 2. `ISchemaCollector` - 3. `IKeyedSchemaCollector` - 4. `ICustomSchemaCollector` -3. Apply some attributes. -4. Register the keyword. -5. Create a JSON converter. - -And your new keyword is ready to use. - -Lastly, remember that the best resource building keywords is [the code](https://github.com/gregsdennis/json-everything/tree/master/JsonSchema) where all of the built-in keywords are defined. - -### Evaluation philosophy - -Starting with version 5 of _JsonSchema.Net_, schema evaluation occurs in two stages: gathering constraints and processing evaluations. Constraints represent all of the work that can be performed by the keyword without an instance, while evaluations complete the work. By separating these stages, _JsonSchema.Net_ can reuse the constraints for subsequent runs, allowing faster run times and fewer memory allocations. - -Both stages are defined by implementing the single method on `IJsonSchemaKeyword`. - -### Ahead of Time (AOT) compatibility {#aot} - -_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). - -First, on your serialization context, you'll need to add `[JsonSerializable]` attributes for any custom keywords. - -```c# -[JsonSerializable(typeof(MyKeyword))] -``` - -Second, you'll need to register your keywords using the `SchemaKeywordRegistry.Register(JsonSerializerContext)` method overload, passing in your serializer context, to provide the library access to the `JsonTypeInfo` for your keyword type. - -Lastly, due to the dynamic nature of how rules are serialized, your JSON converter MUST implement `IWeaklyTypedJsonConverter` which is defined by _Json.More.Net_. The library also defines a `WeaklyTypeJsonConverter` abstract class that you can use as a base. It's also highly recommended that you take advantage of the `JsonSerializerOptions` [read/write extensions](/more/json-more/#ahead-of-time-aot-compilation-support) provided by _Json.More.Net_. - -### 1. Implement `IJsonSchemaKeyword` {#schema-vocabs-custom-keywords-1} - -Implementing your keyword will require some initial thought and design around what work the keyword can perform without the instance and what work requires the instance. To illustrate this, let's look at a couple of the existing keyword implementations. - -#### `maximum` - -The `maximum` keyword is basically all instance. It asks, "Is the instance a number, and, if so, does it exceed some maximum value?" As such, there's not really much in the way of pre-processing that can be accomplished here that isn't handled in the background. Therefore, all of the work is done by an `Evaluator()` method. - -```c# -public KeywordConstraint GetConstraint(SchemaConstraint schemaConstraint, - ReadOnlySpan localConstraints, - EvaluationContext context) -{ - return new KeywordConstraint(Name, Evaluator); -} - -private void Evaluator(KeywordEvaluation evaluation, EvaluationContext context) -{ - var schemaValueType = evaluation.LocalInstance.GetSchemaValueType(); - if (schemaValueType is not (SchemaValueType.Number or SchemaValueType.Integer)) - { - evaluation.MarkAsSkipped(); - return; - } - - var number = evaluation.LocalInstance!.AsValue().GetNumber(); - if (Value < number) - evaluation.Results.Fail(Name, - ErrorMessages.GetMaximum(context.Options.Culture), - ("received", number), - ("limit", Value)); -} -``` - -> Although the `.Evaluator()` method contains an `EvaluationContext` parameter, it's very important that you don't pass the context from the `.GetConstraint()` method. Each evaluation creates a new context object which could carry different options (and schema registries), which could create some unpredictable behaviors. -{: .prompt-warning } - -Here, getting the constraint means just pointing to the evaluation function, which will be called once we have the instance. Behind the scenes, the constraint manages evaluation path, instance location, and some other details. But this is all that's needed for this keyword. - -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. - -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. - -> `maximum` doesn't have any nested results, but it's still good form to explicitly indicate this. -{: .prompt-info } - -#### `properties` - -The `properties` keyword presents an opportunity to calculate some things before we have the instance. For example, with this schema - -```json -{ - "type": "object", - "properties": { - "foo": { "type": "string" }, - "bar": { "type": "number" } - } -} -``` - -we _know_: - -1. that the instance **must** be an object -2. if that object has a `foo` property, its value **must** be a string -3. if that object has a `bar` property, its value **must** be a number - -More specifically to our task here, `properties` gives us a list of subschemas that must validate values at specific instance locations. So for each property listed in the keyword, we need to generate a constraint for the associated subschema. To support this, the `JsonSchema` object exposes a `.GetConstraint()` method of its own that returns a `SchemaConstraint`. - -```c# -public KeywordConstraint GetConstraint(SchemaConstraint schemaConstraint, - ReadOnlySpan localConstraints, - EvaluationContext context) -{ - var subschemaConstraints = Properties - .Select(x => x.Value.GetConstraint(relativeEvaluationPath: JsonPointer.Create(Name, x.Key), - baseInstanceLocation: schemaConstraint.BaseInstanceLocation, - relativeInstanceLocation: JsonPointer.Create(x.Key), - context: context)) - .ToArray(); - - return new KeywordConstraint(Name, Evaluator) - { - ChildDependencies = subschemaConstraints - }; -} - -private static void Evaluator(KeywordEvaluation evaluation, EvaluationContext context) -{ - var annotation = evaluation.ChildEvaluations - .Select(x => (JsonNode)x.RelativeInstanceLocation.Segments[0].Value!) - .ToJsonArray(); - evaluation.Results.SetAnnotation(Name, annotation); - - if (!evaluation.ChildEvaluations.All(x => x.Results.IsValid)) - evaluation.Results.Fail(); -} -``` - -Here you can see there's a lot more going on in the `.GetConstraint()` method than with `maximum`. Because we know the instance locations, although we don't have the instance itself, we can go ahead and set up the constraints for those locations. Once we have an array of subschema constraints, they are added to the keyword constraint's `.ChildDependencies` property. _JsonSchema.Net_ will ensure that those evaluations are processed prior to running the one for this keyword. - -When we move into the evaluation phase, all of the child constraints that align with locations actually present in the instance will have `SchemaEvaluation`s generated for them, which are accessible on the `KeywordEvaluation.ChildEvaluations` property. Because we did a lot of the work up front, to evaluate `properties`, we only need to verify that all of our child evaluations passed. We set an annotation, and then verify. - -> The specification requires that annotations are not reported when validation fails, however this requirement is enforced at the (sub)schema level, not at the keyword level. Annotations are still generally required for sibling keywords (i.e. within the same subschema) to interoperate correctly. -{: .prompt-warning } - -#### Other variations - -There are a few other variations of keyword interactions, and it may be worth inspecting the code for some of these examples. - -- **Pure annotation keywords** - These keywords perform no validation, but instead only apply an annotation. - - `title` & `description` -- **Keyword dependencies** - These communicate mainly through annotations set by the dependent keywords. - - `additionalProperties` depends on `properties` and `patternProperties` - - `then` and `else` depend on `if` -- **Constraint templating** - These keywords have one or more subschemas that each potentially apply to multiple locations which cannot be known without an instance. - - `patternProperties` applies subschemas to all instance locations that match each regular expression - - `additionalItems` applies its subschema to instance properties not addressed by `properties` or `patternProperties` -- **No-op keywords** - Keywords that play no validation or annotation role can be skipped during evaluation. - - `$defs` & `$comment` - - `then` or `else` when `if` isn't present - -> In order to prevent unnecessary allocations, there is a static `KeywordConstraint.Skip` that can be re-used as needed to represent a constraint that doesn't need to do anything. -{: .prompt-tip } - -Understanding the patterns that already exist will help you build your own keyword implementations. - -#### Saving evaluation results - -Once you have validated the instance, you'll need to record the results. These methods are available on the local result object. - -- `Fail()` - Fails the validation without a message -- `Fail(string keyword, string? message)` - Sets a failed validation along with a predefined error message. -- `Fail(string keyword, string message, params (string token, object? value)[] parameters)` - Sets a failed validation along with a templated error message (see [Custom Error Messages](/schema/basics/#schema-errors)) - -> Validation assumes to have passed unless one of these methods is called. -{: .prompt-info } - -Set any annotations by using `.SetAnnotation()` on the local result object. Generally this needs to be done whether the keyword passes or fails validation. Annotations are stored as a key-value pair, using the keyword name as the key. The value can be anything, but it _should_ be JSON-serializable in order to be rendered properly in the output. - -### 2. Implement one of the schema-container interfaces {#schema-vocabs-custom-keywords-2} - -If your keyword contains one or more subschemas, you'll need to implement one of these: - -- `ISchemaContainer` - your keyword simply contains a single schema (e.g. `additionalProperties`) -- `ISchemaCollector` - your keyword contains an array of schemas (e.g. `allOf`) -- `IKeyedSchemaCollector` - your keyword contains a key-value collection of schemas (e.g. `properties`) -- `ICustomSchemaCollector` - your keyword contains schemas in some other arrangement (e.g. `propertyDependencies`) - -These will be used at the beginning of the first evaluation and during schema registration to traverse all of the subschemas a provide IDs where none is explicitly declared. This goes on to help `$ref` and friends to their job while also making that job faster. - -### 3. Apply some attributes {#schema-vocabs-custom-keyword-3} - -*JsonSchema.Net* contains several attributes that you should use to specify some metadata about your keyword. - -- `SchemaKeyword` - Defines the keyword as it appears in the schema. -- `SchemaPriority` - Defines a priority that will be used to order keyword evaluation properly. Keywords with the same priority are evaluated in the order they appear in the schema. -- `SchemaVersion` - Declares a version that supports the keyword. This can be used multiple times to declare additional drafts. -- `Vocabulary` - Declares the ID of the vocabulary which defines the the keyword. - -### 4. Register your keyword {#schema-vocabs-custom-keywords-4} - -To make *JsonSchema.Net* aware of your keyword, you must register it with `SchemaKeywordRegistry.Register()`. This will enable deserialization. - -#### Now make it nice to use {#schema-vocabs-custom-extensions} - -To enable the fluent construction interface for your keyword, simply create an extension method on `JsonSchemaBuilder` that adds the keyword and returns the builder. For example, adding a `description` keyword is implemented by this method: - -```c# -public static JsonSchemaBuilder Description(this JsonSchemaBuilder builder, string description) -{ - builder.Add(new DescriptionKeyword(description)); - return builder; -} -``` - -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: - -```c# -public static string? GetDescription(this JsonSchema schema) -{ - return schema.TryGetKeyword(DescriptionKeyword.Name, out var k) - ? k.Value - : null; -} -``` - -### 5. Create a JSON converter {#schema-vocabs-custom-converter} - -To enable serialization and deserialization, you'll need to provide the converter for it. - -Implement a `JsonConverter` and apply a `JsonConverter` attribute to the keyword. +See [Defining and Using Custom Keywords](custom-keywords) for information on how to create custom keywords and dialects. diff --git a/_docs/schema/vocabs/array-ext.md b/_docs/schema/vocabs/array-ext.md index 1fdb6dc9..f9923880 100644 --- a/_docs/schema/vocabs/array-ext.md +++ b/_docs/schema/vocabs/array-ext.md @@ -4,7 +4,7 @@ title: A Vocabulary for Extended Validation of Arrays bookmark: Array Extensions permalink: /schema/vocabs/array-ext/ icon: fas fa-tag -order: "01.8.2" +order: "01.09.2" --- ## 1. Purpose {#purpose} diff --git a/_docs/schema/vocabs/close.md b/_docs/schema/vocabs/close.md index 0cdcff4f..d259079d 100644 --- a/_docs/schema/vocabs/close.md +++ b/_docs/schema/vocabs/close.md @@ -2,5 +2,5 @@ title: __close permalink: /schema/vocabs/:title/ close: true -order: "01.8.9" +order: "01.09.9" --- diff --git a/_docs/schema/vocabs/data-2022.md b/_docs/schema/vocabs/data-2022.md index 2beb947b..0af9b04f 100644 --- a/_docs/schema/vocabs/data-2022.md +++ b/_docs/schema/vocabs/data-2022.md @@ -4,7 +4,7 @@ title: A Vocabulary for Accessing Data Stored in JSON (2022) bookmark: "[deprecated] data (2022)" permalink: /schema/vocabs/data-2022/ icon: fas fa-tag -order: "01.8.4" +order: "01.09.4" --- > This vocabulary has been deprecated and replaced by [2023 Data vocabulary](/schema/vocabs/data-2023). diff --git a/_docs/schema/vocabs/data-2023.md b/_docs/schema/vocabs/data-2023.md index 97b5a2a3..56fb4e9d 100644 --- a/_docs/schema/vocabs/data-2023.md +++ b/_docs/schema/vocabs/data-2023.md @@ -4,7 +4,7 @@ title: A Vocabulary for Accessing Data Stored in JSON (2023) bookmark: data (2023) permalink: /schema/vocabs/data-2023/ icon: fas fa-tag -order: "01.8.1" +order: "01.09.1" --- ## 1. Purpose {#purpose} diff --git a/_docs/schema/vocabs/examples/close.md b/_docs/schema/vocabs/examples/close.md index d70d9208..71b6f401 100644 --- a/_docs/schema/vocabs/examples/close.md +++ b/_docs/schema/vocabs/examples/close.md @@ -2,5 +2,5 @@ title: __close permalink: /schema/vocabs/examples/:title/ close: true -order: "01.8.6.9" +order: "01.09.6.9" --- diff --git a/_docs/schema/vocabs/examples/data-ref.md b/_docs/schema/vocabs/examples/data-ref.md index f7bb9b5f..f6380c01 100644 --- a/_docs/schema/vocabs/examples/data-ref.md +++ b/_docs/schema/vocabs/examples/data-ref.md @@ -4,7 +4,7 @@ title: Referencing Instance Data bookmark: Instance Data permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.8.6.2" +order: "01.09.6.2" --- Most of the questions that center around referencing data involve comparing the values of properties. This typically comes in a few flavors: diff --git a/_docs/schema/vocabs/examples/external-ref.md b/_docs/schema/vocabs/examples/external-ref.md index 9f773ced..f10dff89 100644 --- a/_docs/schema/vocabs/examples/external-ref.md +++ b/_docs/schema/vocabs/examples/external-ref.md @@ -4,7 +4,7 @@ title: Referencing External Data bookmark: External Data permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.8.6.3" +order: "01.09.6.3" --- Sometimes the values you want to use for schemas are stored in external files. To reference these, you'll need to use a URI. This URI may be combined with a pointer to indicate a location within the file. diff --git a/_docs/schema/vocabs/examples/setup.md b/_docs/schema/vocabs/examples/setup.md index 0bbf5ec9..7b243d48 100644 --- a/_docs/schema/vocabs/examples/setup.md +++ b/_docs/schema/vocabs/examples/setup.md @@ -4,7 +4,7 @@ title: Setting Up Vocabs bookmark: Vocab Setup permalink: /schema/examples/:title/ icon: fas fa-tag -order: "01.8.6.1" +order: "01.09.6.1" --- In order to use the vocabulary extension libraries, there is some initial setup that will need to be performed. diff --git a/_docs/schema/vocabs/examples/title.md b/_docs/schema/vocabs/examples/title.md index 5259e4bc..49913765 100644 --- a/_docs/schema/vocabs/examples/title.md +++ b/_docs/schema/vocabs/examples/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Examples permalink: /schema/vocabs/examples/:title/ folder: true -order: "01.8.6" +order: "01.09.6" --- diff --git a/_docs/schema/vocabs/openapi.md b/_docs/schema/vocabs/openapi.md index dc1eddbb..8625a05d 100644 --- a/_docs/schema/vocabs/openapi.md +++ b/_docs/schema/vocabs/openapi.md @@ -4,7 +4,7 @@ title: OpenAPI v3.1 Vocabulary bookmark: Open API permalink: /schema/vocabs/openapi/ icon: fas fa-tag -order: "01.8.3" +order: "01.09.3" --- This library adds support for the vocabularies, meta-schemas, and keywords defined by the [OpenAPI v3.1 specification](https://spec.openapis.org/oas/latest.html). @@ -12,7 +12,8 @@ This library adds support for the vocabularies, meta-schemas, and keywords defin To enable this vocabulary, include the following in your startup logic to register everything. ```c# -Json.Schema.OpenApi.Vocabularies.Register(); +// optionally takes a build options to add support only to those registries. +Json.Schema.OpenApi.MetaSchemas.Register(); ``` Note that all of the keywords defined by this vocabulary are annotative: they will produce annotations but provide no validation logic. The output from these keywords is intended to be used by other OpenAPI tooling. diff --git a/_docs/schema/vocabs/title.md b/_docs/schema/vocabs/title.md index f9de423c..3863cb78 100644 --- a/_docs/schema/vocabs/title.md +++ b/_docs/schema/vocabs/title.md @@ -3,5 +3,5 @@ title: __title bookmark: Prebuilt Vocabularies permalink: /schema/vocabs/:title/ folder: true -order: "01.8" +order: "01.09" --- diff --git a/_docs/schema/vocabs/unique-keys.md b/_docs/schema/vocabs/unique-keys.md index b63a3f70..a4f2020e 100644 --- a/_docs/schema/vocabs/unique-keys.md +++ b/_docs/schema/vocabs/unique-keys.md @@ -4,7 +4,7 @@ title: A Vocabulary for Identifying Uniqueness of Array Items bookmark: "[deprecated] uniqueKeys" permalink: /schema/vocabs/uniquekeys/ icon: fas fa-tag -order: "01.8.5" +order: "01.09.5" --- > This vocabulary has been deprecated and replaced by the more general [Array Extensions vocabulary](/schema/vocabs/array-ext). diff --git a/jekyll-theme-chirpy.gemspec b/jekyll-theme-chirpy.gemspec index a5ee02fa..62b909ba 100644 --- a/jekyll-theme-chirpy.gemspec +++ b/jekyll-theme-chirpy.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = "jekyll-theme-chirpy" - spec.version = "5.6.1" + spec.version = "7.2.4" spec.authors = ["Cotes Chung"] spec.email = ["cotes.chung@gmail.com"] diff --git a/run.bat b/run.bat index cee07160..da50f769 100644 --- a/run.bat +++ b/run.bat @@ -9,5 +9,5 @@ if "%1" == "prod" ( ) @echo on -bundle exec jekyll serve --incremental +bundle exec jekyll serve --incremental --livereload