Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .jekyll-metadata
Binary file not shown.
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 3 additions & 34 deletions _docs/pointer/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,8 @@ There are three ways create pointers:
```c#
var pointer = JsonPointer.Create("object", "and", 3, "arrays");
```
- building with `Create<T>()` and supplying a LINQ expression (also see [below](#linq))
```c#
var pointer = JsonPointer.Create<MyObject>(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\"}]}}");
Expand Down Expand Up @@ -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<T>()` 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.
Expand Down
256 changes: 89 additions & 167 deletions _docs/schema/basics.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion _docs/schema/close.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
title: __close
permalink: /schema/:title/
close: true
order: "01.9"
order: "01.10"
---
2 changes: 1 addition & 1 deletion _docs/schema/codegen/close.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
title: __close
close: true
permalink: /schema/codegen/:title/
order: "01.6.9"
order: "01.07.9"
---
2 changes: 1 addition & 1 deletion _docs/schema/codegen/mini-meta-schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
>
Expand Down
2 changes: 1 addition & 1 deletion _docs/schema/codegen/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
>
Expand Down
2 changes: 1 addition & 1 deletion _docs/schema/codegen/schema-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
>
Expand Down
2 changes: 1 addition & 1 deletion _docs/schema/codegen/title.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ title: __title
bookmark: Code Generation
permalink: /schema/codegen/:title/
folder: true
order: "01.6"
order: "01.07"
---
83 changes: 83 additions & 0 deletions _docs/schema/custom-keywords.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion _docs/schema/datagen/close.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
title: __close
close: true
permalink: /schema/datagen/:title/
order: "01.7.9"
order: "01.06.9"
---
2 changes: 1 addition & 1 deletion _docs/schema/datagen/schema-datagen.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion _docs/schema/datagen/title.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ title: __title
bookmark: Data Generation
permalink: /schema/datagen/:title/
folder: true
order: "01.7"
order: "01.06"
---
2 changes: 1 addition & 1 deletion _docs/schema/examples/close.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
title: __close
close: true
permalink: /schema/examples/:title/
order: "01.4.9"
order: "01.05.9"
---
110 changes: 110 additions & 0 deletions _docs/schema/examples/custom-keywords.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading