From 8d6338840459135b367a65bfb31797f7bd35c8d3 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Wed, 2 Aug 2023 12:08:20 +1200 Subject: [PATCH] announce jsonchema.net 5 --- _posts/2023/2023-07-26-new-json-schema-net.md | 77 ++++++++++++++++++ assets/img/2023-08-02-minstrels.png | Bin 0 -> 17108 bytes 2 files changed, 77 insertions(+) create mode 100644 _posts/2023/2023-07-26-new-json-schema-net.md create mode 100644 assets/img/2023-08-02-minstrels.png diff --git a/_posts/2023/2023-07-26-new-json-schema-net.md b/_posts/2023/2023-07-26-new-json-schema-net.md new file mode 100644 index 0000000..ecdb0b8 --- /dev/null +++ b/_posts/2023/2023-07-26-new-json-schema-net.md @@ -0,0 +1,77 @@ +--- +title: "The New JsonSchema.Net" +date: 2023-08-02 09:00:00 +1200 +tags: [json-schema, performance] +toc: true +pin: false +--- + +Some changes are coming to _JsonSchema.Net_: faster validation and fewer memory allocations thanks to a new keyword architecture. + +The best part: unless you've built your own keywords, this probably won't require any changes in your code. + +## A new keyword architecture? + +For about the past year or so, I've had an idea that I've tried and failed to implement several times: by performing static analysis of a schema, some work can be performed before ever getting an instance to validate. That work can then be saved and reused across multiple evaluations. + +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 + +These are the _constraints_ that this schema applies to any instance that it validates. Each constraint is comprised of an instance location and a requirement for the corresponding value. What's more, most of the time, we don't need the instance to identify these constraints. + +This is the basic idea behind the upcoming _JsonSchema.Net_ v5 release. If I can capture these constraints and save them, then I only have to perform this analysis once. After that, it's just applying the constraints to the instance. + +## Architecture overview + +With the upcoming changes, evaluating an instance against a schema occurs in two phases: gathering constraints, and processing individual evaluations. + +> For the purposes of this post, I'm going to refer to the evaluation of an individual constraint as simply an "evaluation." +{: .prompt-info } + +### Collecting constraints + +There are two kinds of constraints: schema and keyword. A schema constraint is basically a collection of keyword constraints, but it also needs to contain some things that are either specific to schemas, such as the schema's base URI, or common to all the local constraints, like the instance location. A keyword constraint, in turn, will hold the keyword it represents, any sibling keywords it may have dependencies on, schema constraints for any subschemas the keyword defines, and the actual evaluation function. + +> I started with just the idea of a generic "constraint" object, but I soon found that the two had very different roles, so it made sense to separate them. I think this was probably the key distinction from previous attempts that allowed me to finally make this approach work. +{: .prompt-info } + +So for constraints we have this recursive definition that really just mirrors the structural definition represented by `JsonSchema` and the different keyword classes. The primary difference between the constraints and the structural definition is that the constraints are more generic (implemented by two types) and evaluation-focused, whereas the structural definition is the more object-oriented model and is used for serialization and other things. + +Building a schema constraint consists of building constraints for all of the keywords that (sub)schema contains. Each keyword class knows how to build the constraint that should represent it, including getting constraints for subschemas and identifying keyword dependencies. + +Once we have the full constraint tree, we can save that in the `JsonSchema` object and reuse that work for each evaluation. + +### Evaluation + +Each constraint object produces an associated evaluation object. Again, there are two kinds: one for each kind of constraint. + +When constructing a schema evaluation, we need the instance (of course), the evaluation path, and any options to apply during this evaluation. It's important to recognize that options can change between evaluations; for example, sometimes you may or may not want to validate `format`. A results object for this subschema will automatically be created. Creating a schema evaluation will also call on the contained keyword constraints to build their evaluations. + +To build a keyword evaluation, the keyword constraint is given the schema constraint that's requesting it, the instance location, and the evaluation path. From that, it can look at the instance, determine if the evaluation even needs to run (e.g. is there a value at that instance location?), and create an evaluation object if it does. It will also create schema evaluations for any subschemas. + +In this way, we get another tree: one built for evaluating a specific instance. The structure of this tree may (and often will) differ from the structure of the constraint tree. For example, when building constraints, we don't know what properties `additionalProperties` will need to cover, so we build a template from which we can later create multiple evaluations: one for each property. Or maybe `properties` contains a property that doesn't exist in the instance; no evaluation is created because there's nothing to evaluate. + +While building constraints only happens once, building evaluations occurs every time `JsonSchema.Evaluate()` is called. + +## [And there was much rejoicing](https://www.youtube.com/watch?v=NmPhaG1ud38) + +This a lot, and it's a significant departure from the more procedural approach of previous versions. But I think it's a good change overall because this new design encapsulates forethought and theory present within JSON Schema and uses that to improve performance. + +If you find you're in the expectedly small group of users writing your own keywords, I'm also updating the docs, so you'll have some help there. If you still have questions, feel free to open an issue or you can find me in Slack (link on the repo readme). + +I'm also planning a post for the [JSON Schema blog](https://json-schema.org/blog) which looks at a bit more of the theory of JSON Schema static analysis separately from the context of _JsonSchema.Net_, so watch for that as well. diff --git a/assets/img/2023-08-02-minstrels.png b/assets/img/2023-08-02-minstrels.png new file mode 100644 index 0000000000000000000000000000000000000000..d68d2c9549a4a8490e58aad203fd72fa32846ab6 GIT binary patch literal 17108 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 P00000NkvXXu0mjfWD}Yx literal 0 HcmV?d00001