From cc1cbe168e912a907c8b34f4bf3c423de464da6e Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Mon, 25 Sep 2023 11:25:05 +1300 Subject: [PATCH 01/12] unfinished proposal; add to build --- package.json | 3 +- proposals/propertyDependencies.md | 78 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 proposals/propertyDependencies.md diff --git a/package.json b/package.json index e5e2a18f..96ebfb54 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "build": "npm run build-core && npm run build-validation && npm run build-output", "build-core": "node build/build.js < jsonschema-core.md > jsonschema-core.html", "build-validation": "node build/build.js < jsonschema-validation.md > jsonschema-validation.html", - "build-output": "node build/build.js < jsonschema-validation-output-machines.md > jsonschema-validation-output-machines.html" + "build-output": "node build/build.js < jsonschema-validation-output-machines.md > jsonschema-validation-output-machines.html", + "build-propertyDependencies": "node build/build.js < proposals/propertyDependencies.md > proposals/propertyDependencies.html" }, "license": "MIT", "dependencies": { diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md new file mode 100644 index 00000000..c837e9d5 --- /dev/null +++ b/proposals/propertyDependencies.md @@ -0,0 +1,78 @@ +# JSON Schema Proposal: `propertyDependencies` Keyword + +## Abstract + +This document proposes a change to the JSON Schema Core specification and +Applicator vocabulary by adding the `propertyDependencies` keyword. + +## Note to Readers + +The issues list for this proposal can be found at +. + +For additional information, see . + +To provide feedback, use this issue tracker or any of the communication methods +listed on the homepage. + +## Table of Contents + +## Conventions and Terminology + +All conventions and terms used and defined by the JSON Schema Core specification +also apply to this document. + +## Overview + +### Problem Statement + + + +### Solution + + + +### Alternatives + + + +### Limitations + + + +### Examples + + + +## Proposal + +### Target for Change + + + +This proposal will add the {{propertyDependencies}} section contained herein as +a subsection of JSON Schema Core, section 10.2.2 "Keywords for Applying +Subschemas Conditionally." + + + +### New Keyword: `propertyDependencies` {#propertyDependencies} + +This keyword specifies subschemas that are evaluated if the instance is an +object and contains a certain property with a certain string value. + +This keyword's value MUST be an object. Each value in the object MUST be an +object whose values MUST be valid JSON Schemas. + +If the outer object key is a property in the instance and the inner object key +is equal to the value of that property, the entire instance must validate +against the schema. Its use is dependent on the presence and value of the +property. + +Omitting this keyword has the same behavior as an empty object. + +## Champions + +| Champion | Company | Email | URI | +|----------------------------|---------|-------------------------|----------------------------------| +| Jason Desrosiers | Postman | | | From ee650237d17f74fabf5f4c7c8637647450e6e0d6 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Mon, 25 Sep 2023 11:33:16 +1300 Subject: [PATCH 02/12] fix build script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96ebfb54..e7808b5d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "lint": "eslint build/", - "build": "npm run build-core && npm run build-validation && npm run build-output", + "build": "npm run build-core && npm run build-validation && npm run build-output && npm run build-propertyDependencies", "build-core": "node build/build.js < jsonschema-core.md > jsonschema-core.html", "build-validation": "node build/build.js < jsonschema-validation.md > jsonschema-validation.html", "build-output": "node build/build.js < jsonschema-validation-output-machines.md > jsonschema-validation-output-machines.html", From a5f17b6af5220ceb6c7030e065608213f0244e5b Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Mon, 25 Sep 2023 11:40:02 +1300 Subject: [PATCH 03/12] normalize header; ignore built html file --- .gitignore | 1 + proposals/propertyDependencies.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 47062da0..593a560e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ jsonschema-*.txt relative-json-pointer.html relative-json-pointer.pdf relative-json-pointer.txt +proposals/*.html # For the Python enviornment .venv diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index c837e9d5..77bec298 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -50,13 +50,13 @@ also apply to this document. -This proposal will add the {{propertyDependencies}} section contained herein as +This proposal will add the {{propertydependencies}} section contained herein as a subsection of JSON Schema Core, section 10.2.2 "Keywords for Applying Subschemas Conditionally." -### New Keyword: `propertyDependencies` {#propertyDependencies} +### New Keyword: `propertyDependencies` {#propertydependencies} This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property with a certain string value. From 19e9102a78fbc1d8a6a7fefeee55de0ce0ce963a Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Mon, 25 Sep 2023 11:58:48 +1300 Subject: [PATCH 04/12] remove hints from proposal section --- proposals/propertyDependencies.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index 77bec298..abc623a3 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -48,14 +48,10 @@ also apply to this document. ### Target for Change - - This proposal will add the {{propertydependencies}} section contained herein as a subsection of JSON Schema Core, section 10.2.2 "Keywords for Applying Subschemas Conditionally." - - ### New Keyword: `propertyDependencies` {#propertydependencies} This keyword specifies subschemas that are evaluated if the instance is an From ffee0cfff45638a62ca8da4f4537d5fad79436f2 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 3 Oct 2023 15:12:32 -0700 Subject: [PATCH 05/12] Update the build process to support more files --- README.md | 8 +++++--- build/build.js | 22 +++++++++++++++++++--- package.json | 7 ++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index af091291..d57a233b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ Labels are assigned based on [Sensible Github Labels](https://github.com/Releque ## Authoring and Building ### Specification -To build the spec files to HTML from the Markdown sources, run `npm run build`. -You can also build each individually with `npm run build-core` and `npm run -build-validation`. +To build the spec files to HTML from the Markdown sources, run `npm run +build-all`. +You can also build each individually with `npm run build -- filename.md` +(Example: `npm run build -- jsonschema-core.md`). You can also use wildcards to +build multiple specs at the same time: `npm run build -- jsonschema-*.md`. The spec is built using [Remark](https://remark.js.org/), a markdown engine with good support for plugins and lots of existing plugins we can use. diff --git a/build/build.js b/build/build.js index e8b3f0ef..ee0a00c5 100644 --- a/build/build.js +++ b/build/build.js @@ -1,5 +1,7 @@ import dotenv from "dotenv"; import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, basename } from "node:path"; +import { argv } from "node:process"; import { reporter } from "vfile-reporter"; import { remark } from "remark"; import remarkCodeTitles from "./remark-code-titles.js"; @@ -18,8 +20,8 @@ import rehypeStringify from "rehype-stringify"; dotenv.config(); -(async function () { - const md = readFileSync(0, "utf-8"); +const build = async (filename) => { + const md = readFileSync(filename, "utf-8"); const html = await remark() .use(remarkPresetLintMarkdownStyleGuide) .use(remarkGfm) @@ -41,7 +43,8 @@ dotenv.config(); .use(rehypeStringify) .process(md); - writeFileSync(1, ` + const outfile = `${dirname(filename)}/${basename(filename, ".md")}.html`; + writeFileSync(outfile, ` @@ -149,4 +152,17 @@ dotenv.config(); `); console.error(reporter(html)); +}; + +(async function () { + const files = argv.slice(2); + if (files.length === 0) { + console.error("WARNING: No files built. Usage: 'npm run build -- filename.md'"); + } + + for (const filename of files) { + console.log(`Building: ${filename} ...`); + await build(filename); + console.log(""); + } }()); diff --git a/package.json b/package.json index e7808b5d..fe675c71 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,8 @@ "main": "index.js", "scripts": { "lint": "eslint build/", - "build": "npm run build-core && npm run build-validation && npm run build-output && npm run build-propertyDependencies", - "build-core": "node build/build.js < jsonschema-core.md > jsonschema-core.html", - "build-validation": "node build/build.js < jsonschema-validation.md > jsonschema-validation.html", - "build-output": "node build/build.js < jsonschema-validation-output-machines.md > jsonschema-validation-output-machines.html", - "build-propertyDependencies": "node build/build.js < proposals/propertyDependencies.md > proposals/propertyDependencies.html" + "build-all": "node build/build.js jsonschema-*.md proposals/*.md", + "build": "node build/build.js" }, "license": "MIT", "dependencies": { From 6be47fa3562882d68dd26d44d435a9d5e6b1f318 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 3 Oct 2023 20:27:08 -0700 Subject: [PATCH 06/12] Fill in missing sections of propDeps proposal and refactor --- build/build.js | 20 ++- jsonschema-core.md | 19 +-- proposals/propertyDependencies.md | 268 +++++++++++++++++++++++++++--- 3 files changed, 263 insertions(+), 44 deletions(-) diff --git a/build/build.js b/build/build.js index ee0a00c5..550b0008 100644 --- a/build/build.js +++ b/build/build.js @@ -28,7 +28,16 @@ const build = async (filename) => { .use(remarkHeadingId) .use(remarkHeadings, { startDepth: 2, - skip: ["Abstract", "Note to Readers", "Table of Contents", "Authors' Addresses", "\\[.*\\]", "draft-.*"] + skip: [ + "Abstract", + "Status", + "Note to Readers", + "Table of Contents", + "Authors' Addresses", + "Champions", + "\\[.*\\]", + "draft-.*" + ] }) .use(remarkReferenceLinks) .use(remarkFlexibleContainers) @@ -36,7 +45,14 @@ const build = async (filename) => { .use(remarkTorchLight) .use(remarkTableOfContents, { startDepth: 2, - skip: ["Abstract", "Note to Readers", "\\[.*\\]", "Authors' Addresses", "draft-.*"] + skip: [ + "Abstract", + "Note to Readers", + "Authors' Addresses", + "Champions", + "\\[.*\\]", + "draft-.*" + ] }) .use(remarkValidateLinks) .use(remarkRehype) diff --git a/jsonschema-core.md b/jsonschema-core.md index 700b90fa..8f184591 100644 --- a/jsonschema-core.md +++ b/jsonschema-core.md @@ -1655,7 +1655,7 @@ User-Agent: product-name/5.4.1 so-cool-json-schema/1.0.2 curl/7.43.0 Clients SHOULD be able to make requests with a "From" header so that server operators can contact the owner of a potentially misbehaving script. -## A Vocabulary for Applying Subschemas +## A Vocabulary for Applying Subschemas {#applicatorvocab} This section defines a vocabulary of applicator keywords that are RECOMMENDED for use as the basis of other vocabularies. @@ -1793,7 +1793,7 @@ successfully validates against its subschema. Implementations MUST NOT evaluate the instance against this keyword, for either validation or annotation collection purposes, in such cases. -##### `dependentSchemas` +##### `dependentSchemas` {#dependent-schemas} This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property. @@ -1807,21 +1807,6 @@ property. Omitting this keyword has the same behavior as an empty object. -##### `propertyDependencies` - -This keyword specifies subschemas that are evaluated if the instance is an -object and contains a certain property with a certain string value. - -This keyword's value MUST be an object. Each value in the object MUST be an -object whose values MUST be valid JSON Schemas. - -If the outer object key is a property in the instance and the inner object key -is equal to the value of that property, the entire instance must validate -against the schema. Its use is dependent on the presence and value of the -property. - -Omitting this keyword has the same behavior as an empty object. - ### Keywords for Applying Subschemas to Child Instances Each of these keywords defines a rule for applying its subschema(s) to child diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index abc623a3..0cf8c72f 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -1,13 +1,24 @@ -# JSON Schema Proposal: `propertyDependencies` Keyword +# JSON Schema: The `propertyDependencies` Keyword ## Abstract -This document proposes a change to the JSON Schema Core specification and -Applicator vocabulary by adding the `propertyDependencies` keyword. +The `propertyDependencies` keyword is a more friendly way to select between two +or more schemas to validate an instance against than is currently supported by +JSON Schema. + +## Status + +**Current Status**: PROPOSAL + +TODO: We should have a short standard blurb outlining the stages involved in a +feature making its way to stable status. + +TODO: Link to a document that describes the proposal => stable process in +detail. ## Note to Readers -The issues list for this proposal can be found at +The issues list for this document can be found at . For additional information, see . @@ -19,40 +30,70 @@ listed on the homepage. ## Conventions and Terminology -All conventions and terms used and defined by the JSON Schema Core specification -also apply to this document. +All conventions and terms used and defined by the [JSON Schema Core +specification](../jsonschema-core.html) also apply to this document. ## Overview ### Problem Statement +A common need in JSON Schema is to select between one schema or another to +validate an instance based on the value of some property in the JSON instance. +There are a several patterns people use to accomplish this, but they all have +significant [problems](#problems). - +OpenAPI solves this problem with the `discriminator` keyword. However, their +approach is more oriented toward code generation concerns, is poorly specified +when it comes to validation, and is couple to OpenAPI concepts that don't exist +is JSON Schema. Therefore, it's necessary to define something new rather than +adopt `discriminator`. ### Solution - +The `dependentSchemas` keyword is very close to what is needed except it checks +for the presence of a property rather than it's value. The chosen solution is to +build on that concept to solve this problem. -### Alternatives +```jsonschema +{ + "propertyDependencies": { + "foo": { + "aaa": { "$ref": "#/$defs/foo-aaa" } + } + } +} +``` - +The validation result is equivalent to the following schema. -### Limitations - - +```jsonschema +{ + "if": { + "properties": { + "foo": { "const": "aaa" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-aaa" } +} +``` -### Examples - - +### Limitations -## Proposal +The problem of choosing an alternative based on a property value could apply for +a value of any JSON type, but `propertyDependencies` only solves this problem +when the value is a string. One of the main goals of this keyword is to define +something that's intuitive enough and easy enough to use that people will +actually use it rather than fallback to `oneOf` because it's simple. Achieving +those goals means that some trade-offs need to be made. {{alternatives}} lists +some alternatives that we considered. -### Target for Change +## A Vocabulary for Applying Subschemas -This proposal will add the {{propertydependencies}} section contained herein as -a subsection of JSON Schema Core, section 10.2.2 "Keywords for Applying -Subschemas Conditionally." +This document adds the `propertyDependencies` keyword to the +`https://json-schema.org/vocab/applicator` [applicator +vocabulary](../jsonschema-core.html#applicatorvocab). -### New Keyword: `propertyDependencies` {#propertydependencies} +### `propertyDependencies` This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property with a certain string value. @@ -67,8 +108,185 @@ property. Omitting this keyword has the same behavior as an empty object. +## [Appendix] Problems With Existing Patterns {#problems} + +### `oneOf`/`anyOf` + +The pattern of using `oneOf` to describe a choice between two schemas has become +ubiquitous. + +```jsonschema +{ + "oneOf": [ + { "$ref": "#/$defs/aaa" }, + { "$ref": "#/$defs/bbb" } + ] +} +``` + +However, this pattern has several shortcomings. The main problem is that it +tends to produce confusing error messages. Some implementations employ +heuristics to guess the user's intent and provide better messaging, but that's +not wide-spread or consistent behavior, nor is it expected or required from +implementations. + +This pattern is also inefficient. Generally, there is a single value in the +object that determines which alternative to chose, but the `oneOf` pattern has +no way to specify what that value is and therefore needs to evaluate the entire +schema. This is made worse in that every alternative needs to be fully validated +to ensure that only one of the alternative passes and all the others fail. This +last problem can be avoided by using `anyOf` instead, but that pattern is much +less used. + +### `if`/`then` + +We can describe this kind of constraint more efficiently and with with better +error messaging by using `if`/`then`. This allows the user to explicitly specify +the constraint to be used to select which alternative the schema should be used +to validate the schema. However, this pattern has problems of it's own. It's +verbose, error prone, and not particularly intuitive, which leads most people to +avoid it. + +```jsonschema +{ + "allOf": [ + { + "if": { + "properties": { + "foo": { "const": "aaa" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-aaa" } + }, + { + "if": { + "properties": { + "foo": { "const": "bbb" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` + +## [Appendix] Alternatives Considered {#alternatives} + +Here are some alternatives that were considered that support all value types. +All examples have the same validation behavior as the examples above. + +This version uses an array of objects. Each object is a collection of the +variables needed to express a property dependency. This doesn't fit the style of +JSON Schema. There aren't any keywords remotely like this. It's also still too +verbose. It's a little more intuitive than `if`/`then` and definitely less error +prone. + +```jsonschema +{ + "propertyDependencies": [ + { + "propertyName": "foo", + "propertySchema": { "const": "aaa" }, + "apply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertyName": "foo", + "propertySchema": { "const": "bbb" }, + "apply": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` + +A slight variation on that example is to make it a map of keyword to dependency +object. It's still too verbose. + +```jsonschema +{ + "propertyDependencies": { + "foo": [ + { + "propertySchema": { "const": "aaa" }, + "apply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertySchema": { "const": "bbb" }, + "apply": { "$ref": "#/$defs/foo-bbb" } + } + ] + } +} +``` + +This one is a little more consistent with the JSON Schema style (poor keyword +naming aside), but otherwise has all the same problems as the other examples. + +```jsonschema +{ + "allOf": [ + { + "propertyDependencyName": "foo", + "propertyDependencySchema": { "const": "aaa" }, + "propertyDependencyApply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertyDependencyName": "foo", + "propertyDependencySchema": { "const": "bbb" }, + "propertyDependencyApply": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` + +This one is a variation of `if` that combines `if`, `properties`, and `required` +to reduce boilerplate. It's also essentially a variation of the previous example +with better names. This avoids to error proneness problem, but it's still too +verbose. + +```jsonschema +{ + "allOf": [ + { + "ifProperties": { + "foo": { "const": "aaa" } + }, + "then": { "$ref": "#/$defs/foo-aaa" } + }, + { + "ifProperties": { + "foo": { "const": "bbb" } + }, + "then": { "$ref": "#/$defs/foo-aaa" } + } + ] +} +``` + +All of the previous alternatives use a schema as the discriminator. This +alternative is a little less powerful in that it can only match on exact values, +but it successfully addresses the problems we're concerned about with the +current approaches. The only issue with this alternative is that it's not as +intuitive as the chosen solution. + +```jsonschema +{ + "propertyDepenencies": { + "foo": [ + ["aaa", { "$ref": "#/$defs/foo-aaa" }], + ["bbb", { "$ref": "#/$defs/foo-bbb" }] + ] + } +} +``` + +## [Appendix] Change Log + +* [October 2023] Created + ## Champions -| Champion | Company | Email | URI | -|----------------------------|---------|-------------------------|----------------------------------| -| Jason Desrosiers | Postman | | | +| Champion | Company | Email | URI | +|----------------------------|---------|----------------------|----------------------------------| +| Jason Desrosiers | Postman | | | From c93168679d16de76ac6848f55add5ef9cbe427a0 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Wed, 8 May 2024 12:50:39 +1200 Subject: [PATCH 07/12] update proposal for separate ADR --- proposals/propertyDependencies.md | 182 +++++++----------------------- 1 file changed, 38 insertions(+), 144 deletions(-) diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index 0cf8c72f..873127f5 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -1,4 +1,4 @@ -# JSON Schema: The `propertyDependencies` Keyword +# JSON Schema Proposal: The `propertyDependencies` Keyword ## Abstract @@ -6,16 +6,6 @@ The `propertyDependencies` keyword is a more friendly way to select between two or more schemas to validate an instance against than is currently supported by JSON Schema. -## Status - -**Current Status**: PROPOSAL - -TODO: We should have a short standard blurb outlining the stages involved in a -feature making its way to stable status. - -TODO: Link to a document that describes the proposal => stable process in -detail. - ## Note to Readers The issues list for this document can be found at @@ -36,6 +26,7 @@ specification](../jsonschema-core.html) also apply to this document. ## Overview ### Problem Statement + A common need in JSON Schema is to select between one schema or another to validate an instance based on the value of some property in the JSON instance. There are a several patterns people use to accomplish this, but they all have @@ -43,9 +34,9 @@ significant [problems](#problems). OpenAPI solves this problem with the `discriminator` keyword. However, their approach is more oriented toward code generation concerns, is poorly specified -when it comes to validation, and is couple to OpenAPI concepts that don't exist +when it comes to validation, and is coupled to OpenAPI concepts that don't exist is JSON Schema. Therefore, it's necessary to define something new rather than -adopt `discriminator`. +adopt or redefine `discriminator`. ### Solution @@ -53,7 +44,7 @@ The `dependentSchemas` keyword is very close to what is needed except it checks for the presence of a property rather than it's value. The chosen solution is to build on that concept to solve this problem. -```jsonschema +```json { "propertyDependencies": { "foo": { @@ -65,7 +56,7 @@ build on that concept to solve this problem. The validation result is equivalent to the following schema. -```jsonschema +```json { "if": { "properties": { @@ -87,26 +78,38 @@ actually use it rather than fallback to `oneOf` because it's simple. Achieving those goals means that some trade-offs need to be made. {{alternatives}} lists some alternatives that we considered. -## A Vocabulary for Applying Subschemas - -This document adds the `propertyDependencies` keyword to the -`https://json-schema.org/vocab/applicator` [applicator -vocabulary](../jsonschema-core.html#applicatorvocab). - -### `propertyDependencies` - -This keyword specifies subschemas that are evaluated if the instance is an -object and contains a certain property with a certain string value. - -This keyword's value MUST be an object. Each value in the object MUST be an -object whose values MUST be valid JSON Schemas. - -If the outer object key is a property in the instance and the inner object key -is equal to the value of that property, the entire instance must validate -against the schema. Its use is dependent on the presence and value of the -property. - -Omitting this keyword has the same behavior as an empty object. +## Change Description + +1. The following will be added to the JSON Schema Core specification as a +subsection of "Keywords for Applying Subschemas Conditionally". + > ### `propertyDependencies` + > + > This keyword specifies subschemas that are evaluated if the instance is an + > object and contains a certain property with a certain string value. + > + > This keyword's value MUST be an object. Each value in the object MUST be an + > object whose values MUST be valid JSON Schemas. + > + > If the outer object key is a property in the instance and the inner object key + > is equal to the value of that property, the entire instance must validate + > against the schema. Its use is dependent on the presence and value of the + > property. + > + > Omitting this keyword has the same behavior as an empty object. +2. The following subschema will be added to the Applicator Vocabulary schema, `https://json-schema.org///meta/applicator` at `/properties/propertyDependencies`: + ```json + { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta", + "default": true + }, + "default": {} + } + } + ``` ## [Appendix] Problems With Existing Patterns {#problems} @@ -172,115 +175,6 @@ avoid it. } ``` -## [Appendix] Alternatives Considered {#alternatives} - -Here are some alternatives that were considered that support all value types. -All examples have the same validation behavior as the examples above. - -This version uses an array of objects. Each object is a collection of the -variables needed to express a property dependency. This doesn't fit the style of -JSON Schema. There aren't any keywords remotely like this. It's also still too -verbose. It's a little more intuitive than `if`/`then` and definitely less error -prone. - -```jsonschema -{ - "propertyDependencies": [ - { - "propertyName": "foo", - "propertySchema": { "const": "aaa" }, - "apply": { "$ref": "#/$defs/foo-aaa" } - }, - { - "propertyName": "foo", - "propertySchema": { "const": "bbb" }, - "apply": { "$ref": "#/$defs/foo-bbb" } - } - ] -} -``` - -A slight variation on that example is to make it a map of keyword to dependency -object. It's still too verbose. - -```jsonschema -{ - "propertyDependencies": { - "foo": [ - { - "propertySchema": { "const": "aaa" }, - "apply": { "$ref": "#/$defs/foo-aaa" } - }, - { - "propertySchema": { "const": "bbb" }, - "apply": { "$ref": "#/$defs/foo-bbb" } - } - ] - } -} -``` - -This one is a little more consistent with the JSON Schema style (poor keyword -naming aside), but otherwise has all the same problems as the other examples. - -```jsonschema -{ - "allOf": [ - { - "propertyDependencyName": "foo", - "propertyDependencySchema": { "const": "aaa" }, - "propertyDependencyApply": { "$ref": "#/$defs/foo-aaa" } - }, - { - "propertyDependencyName": "foo", - "propertyDependencySchema": { "const": "bbb" }, - "propertyDependencyApply": { "$ref": "#/$defs/foo-bbb" } - } - ] -} -``` - -This one is a variation of `if` that combines `if`, `properties`, and `required` -to reduce boilerplate. It's also essentially a variation of the previous example -with better names. This avoids to error proneness problem, but it's still too -verbose. - -```jsonschema -{ - "allOf": [ - { - "ifProperties": { - "foo": { "const": "aaa" } - }, - "then": { "$ref": "#/$defs/foo-aaa" } - }, - { - "ifProperties": { - "foo": { "const": "bbb" } - }, - "then": { "$ref": "#/$defs/foo-aaa" } - } - ] -} -``` - -All of the previous alternatives use a schema as the discriminator. This -alternative is a little less powerful in that it can only match on exact values, -but it successfully addresses the problems we're concerned about with the -current approaches. The only issue with this alternative is that it's not as -intuitive as the chosen solution. - -```jsonschema -{ - "propertyDepenencies": { - "foo": [ - ["aaa", { "$ref": "#/$defs/foo-aaa" }], - ["bbb", { "$ref": "#/$defs/foo-bbb" }] - ] - } -} -``` - ## [Appendix] Change Log * [October 2023] Created From 2b5c3061844d394f4db2df5900f90cd28cad16cf Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Tue, 14 May 2024 09:12:50 +1200 Subject: [PATCH 08/12] Update proposals/propertyDependencies.md Co-authored-by: Jason Desrosiers --- proposals/propertyDependencies.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index 873127f5..5cda553d 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -75,8 +75,7 @@ a value of any JSON type, but `propertyDependencies` only solves this problem when the value is a string. One of the main goals of this keyword is to define something that's intuitive enough and easy enough to use that people will actually use it rather than fallback to `oneOf` because it's simple. Achieving -those goals means that some trade-offs need to be made. {{alternatives}} lists -some alternatives that we considered. +those goals means that some trade-offs need to be made. ## Change Description From 25aafc717a95ca373dfce46f601906584f80c3ad Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Tue, 14 May 2024 09:14:35 +1200 Subject: [PATCH 09/12] Update proposals/propertyDependencies.md Co-authored-by: Jason Desrosiers --- proposals/propertyDependencies.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index 5cda553d..850696bc 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -79,6 +79,9 @@ those goals means that some trade-offs need to be made. ## Change Description +The `propertyDependencies` keyword will be added to the `https://json-schema.org/vocab/applicator` [applicator +vocabulary](../jsonschema-core.html#applicatorvocab). + 1. The following will be added to the JSON Schema Core specification as a subsection of "Keywords for Applying Subschemas Conditionally". > ### `propertyDependencies` From c566004052e5ebc114b84cac405d867437e55635 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Tue, 14 May 2024 10:56:44 +1200 Subject: [PATCH 10/12] add propertyDependencies adr --- proposals/propertyDependencies-adr.md | 314 ++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 proposals/propertyDependencies-adr.md diff --git a/proposals/propertyDependencies-adr.md b/proposals/propertyDependencies-adr.md new file mode 100644 index 00000000..7e94cdc7 --- /dev/null +++ b/proposals/propertyDependencies-adr.md @@ -0,0 +1,314 @@ +# [short title of solved problem and solution] + +* Status: proposed +* Deciders: @gregsdennis, @jdesrosiers, @relequestual +* Date: 2022-04-07 + +Technical Story: + +- Issue discussing feature - https://github.com/json-schema-org/json-schema-spec/issues/1082 +- PR to add to the spec - https://github.com/json-schema-org/json-schema-spec/issues/1082 +- ADR to extract from the spec and use feature life cycle - https://github.com/json-schema-org/json-schema-spec/pull/1505 + +## Context and Problem Statement + +A common need in JSON Schema is to select between one schema or another to +validate an instance based on the value of some property in the JSON instance. +There are a several patterns people use to accomplish this, but they all have +significant [problems](#problems). + +OpenAPI solves this problem with the `discriminator` keyword. However, their +approach is more oriented toward code generation concerns, is poorly specified +when it comes to validation, and is coupled to OpenAPI concepts that don't exist +is JSON Schema. Therefore, it's necessary to define something new rather than +adopt or redefine `discriminator`. + +## Decision Drivers + +- Ease of use +- Readability +- Coverage of most common use cases +- Coverage of all use cases +- Ease of implementation + +## Considered Options + +All of the following options have the same validation result as the following schema. + +```json +{ + "if": { + "properties": { + "foo": { "const": "aaa" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-aaa" } +} +``` + + +### Option 1 + +The `dependentSchemas` keyword is very close to what is needed except it checks +for the presence of a property rather than it's value. This option builds on +that concept to solve this problem. + +```json +{ + "propertyDependencies": { + "foo": { + "aaa": { "$ref": "#/$defs/foo-aaa" } + } + } +} +``` + +### Option 2 + +This version uses an array of objects. Each object is a collection of the +variables needed to express a property dependency. This doesn't fit the style of +JSON Schema. There aren't any keywords remotely like this. It's also still too +verbose. It's a little more intuitive than `if`/`then` and definitely less error +prone. + +```jsonschema +{ + "propertyDependencies": [ + { + "propertyName": "foo", + "propertySchema": { "const": "aaa" }, + "apply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertyName": "foo", + "propertySchema": { "const": "bbb" }, + "apply": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` + +### Option 3 + +A slight variation on that example is to make it a map of keyword to dependency +object. It's still too verbose. + +```jsonschema +{ + "propertyDependencies": { + "foo": [ + { + "propertySchema": { "const": "aaa" }, + "apply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertySchema": { "const": "bbb" }, + "apply": { "$ref": "#/$defs/foo-bbb" } + } + ] + } +} +``` + +### Option 4 + +This one is a little more consistent with the JSON Schema style (poor keyword +naming aside), but otherwise has all the same problems as the other examples. + +```jsonschema +{ + "allOf": [ + { + "propertyDependencyName": "foo", + "propertyDependencySchema": { "const": "aaa" }, + "propertyDependencyApply": { "$ref": "#/$defs/foo-aaa" } + }, + { + "propertyDependencyName": "foo", + "propertyDependencySchema": { "const": "bbb" }, + "propertyDependencyApply": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` + +### Option 4 + +This one is a variation of `if` that combines `if`, `properties`, and `required` +to reduce boilerplate. It's also essentially a variation of the previous example +with better names. This avoids to error proneness problem, but it's still too +verbose. + +```jsonschema +{ + "allOf": [ + { + "ifProperties": { + "foo": { "const": "aaa" } + }, + "then": { "$ref": "#/$defs/foo-aaa" } + }, + { + "ifProperties": { + "foo": { "const": "bbb" } + }, + "then": { "$ref": "#/$defs/foo-aaa" } + } + ] +} +``` + +### Option 6 + +All of the previous alternatives use a schema as the discriminator. This +alternative is a little less powerful in that it can only match on exact values, +but it successfully addresses the problems we're concerned about with the +current approaches. The only issue with this alternative is that it's not as +intuitive as the chosen solution. + +```jsonschema +{ + "propertyDependencies": { + "foo": [ + ["aaa", { "$ref": "#/$defs/foo-aaa" }], + ["bbb", { "$ref": "#/$defs/foo-bbb" }] + ] + } +} +``` + + +## Decision Outcome + +Option 1 was chosen because it satisfies the most common use cases while being +sufficiently readable and easy to implement, even though it does not satisfy +_all_ use cases, such as those where the property value is not a string. As +these cases are significantly less common, the requirement to support all use +cases carried a lower priority. + +### Positive Consequences + +- Some level of built-in support for a `discriminator`-like keyword that aligns + with the existing operation of JSON Schema. + +### Negative Consequences + +- Properties with non-string values cannot be supported using this keyword and + the `allOf`-`if`-`then` pattern must still be used. + +## Pros and Cons of the Options + +### [option 1] + +[example | description | pointer to more information | …] + +* Good, because it handle the most common use case: string property values +* Good, because all property values are grouped together +* Good, because it's less verbose +* Bad, because it doesn't handle non-string property values + +### [option 2] + +* Good, because it supports all use cases +* Bad, because properties are not naturally grouped together +* Bad, because it's quite verbose +* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. + +### [option 3] + +* Good, because it supports all use cases +* Good, because all property values are grouped together +* Bad, because it's quite verbose +* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. + +### [option 4] + +* Good, because it supports all use cases +* Bad, because properties are not naturally grouped together +* Bad, because it's very verbose +* Bad, because it introduces a lot of inter-keyword dependencies, which we'd have to exhaustively define + +### [option 5] + +* Good, because it supports all use cases +* Good, because it's a familiar syntax +* Bad, because properties are not naturally grouped together +* Bad, because it's very verbose +* Bad, because `ifProperties` is very niche. Will this spawn a new series of `if*` keywords? How would it interact with `if`? + +### [option 6] + +* Good, because it supports all use cases +* Bad, because it's an unintuitive syntax and easy to get wrong +* Bad, because properties are not naturally grouped together + +## Links + +* [Link type] [Link to ADR] +* … + + +## [Appendix] Problems With Existing Patterns {#problems} + +### `oneOf`/`anyOf` + +The pattern of using `oneOf` to describe a choice between two schemas has become +ubiquitous. + +```jsonschema +{ + "oneOf": [ + { "$ref": "#/$defs/aaa" }, + { "$ref": "#/$defs/bbb" } + ] +} +``` + +However, this pattern has several shortcomings. The main problem is that it +tends to produce confusing error messages. Some implementations employ +heuristics to guess the user's intent and provide better messaging, but that's +not wide-spread or consistent behavior, nor is it expected or required from +implementations. + +This pattern is also inefficient. Generally, there is a single value in the +object that determines which alternative to chose, but the `oneOf` pattern has +no way to specify what that value is and therefore needs to evaluate the entire +schema. This is made worse in that every alternative needs to be fully validated +to ensure that only one of the alternative passes and all the others fail. This +last problem can be avoided by using `anyOf` instead, but that pattern is much +less used. + +### `if`/`then` + +We can describe this kind of constraint more efficiently and with with better +error messaging by using `if`/`then`. This allows the user to explicitly specify +the constraint to be used to select which alternative the schema should be used +to validate the schema. However, this pattern has problems of it's own. It's +verbose, error prone, and not particularly intuitive, which leads most people to +avoid it. + +```jsonschema +{ + "allOf": [ + { + "if": { + "properties": { + "foo": { "const": "aaa" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-aaa" } + }, + { + "if": { + "properties": { + "foo": { "const": "bbb" } + }, + "required": ["foo"] + }, + "then": { "$ref": "#/$defs/foo-bbb" } + } + ] +} +``` From 4374400405bf88d55a805221ad46f4734c895aa3 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Wed, 15 May 2024 08:34:30 +1200 Subject: [PATCH 11/12] update propertyDependencies ADR --- proposals/propertyDependencies-adr.md | 85 ++++++++++----------------- 1 file changed, 31 insertions(+), 54 deletions(-) diff --git a/proposals/propertyDependencies-adr.md b/proposals/propertyDependencies-adr.md index 7e94cdc7..cb30be46 100644 --- a/proposals/propertyDependencies-adr.md +++ b/proposals/propertyDependencies-adr.md @@ -1,4 +1,4 @@ -# [short title of solved problem and solution] +# Add New Keyword: `propertyDependencies` * Status: proposed * Deciders: @gregsdennis, @jdesrosiers, @relequestual @@ -64,6 +64,11 @@ that concept to solve this problem. } ``` +* Good, because it handle the most common use case: string property values +* Good, because all property values are grouped together +* Good, because it's less verbose +* Bad, because it doesn't handle non-string property values + ### Option 2 This version uses an array of objects. Each object is a collection of the @@ -89,6 +94,11 @@ prone. } ``` +* Good, because it supports all use cases +* Bad, because properties are not naturally grouped together +* Bad, because it's quite verbose +* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. + ### Option 3 A slight variation on that example is to make it a map of keyword to dependency @@ -111,6 +121,11 @@ object. It's still too verbose. } ``` +* Good, because it supports all use cases +* Good, because all property values are grouped together +* Bad, because it's quite verbose +* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. + ### Option 4 This one is a little more consistent with the JSON Schema style (poor keyword @@ -133,7 +148,12 @@ naming aside), but otherwise has all the same problems as the other examples. } ``` -### Option 4 +* Good, because it supports all use cases +* Bad, because properties are not naturally grouped together +* Bad, because it's very verbose +* Bad, because it introduces a lot of inter-keyword dependencies, which we'd have to exhaustively define + +### Option 5 This one is a variation of `if` that combines `if`, `properties`, and `required` to reduce boilerplate. It's also essentially a variation of the previous example @@ -159,6 +179,12 @@ verbose. } ``` +* Good, because it supports all use cases +* Good, because it's a familiar syntax +* Bad, because properties are not naturally grouped together +* Bad, because it's very verbose +* Bad, because `ifProperties` is very niche. Will this spawn a new series of `if*` keywords? How would it interact with `if`? + ### Option 6 All of the previous alternatives use a schema as the discriminator. This @@ -178,6 +204,9 @@ intuitive as the chosen solution. } ``` +* Good, because it supports all use cases +* Bad, because it's an unintuitive syntax and easy to get wrong +* Bad, because properties are not naturally grouped together ## Decision Outcome @@ -197,58 +226,6 @@ cases carried a lower priority. - Properties with non-string values cannot be supported using this keyword and the `allOf`-`if`-`then` pattern must still be used. -## Pros and Cons of the Options - -### [option 1] - -[example | description | pointer to more information | …] - -* Good, because it handle the most common use case: string property values -* Good, because all property values are grouped together -* Good, because it's less verbose -* Bad, because it doesn't handle non-string property values - -### [option 2] - -* Good, because it supports all use cases -* Bad, because properties are not naturally grouped together -* Bad, because it's quite verbose -* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. - -### [option 3] - -* Good, because it supports all use cases -* Good, because all property values are grouped together -* Bad, because it's quite verbose -* Bad, because we have no precedent for a keyword which explicitly defines its own properties. This would be new operational functionality, which we try to avoid if we can. - -### [option 4] - -* Good, because it supports all use cases -* Bad, because properties are not naturally grouped together -* Bad, because it's very verbose -* Bad, because it introduces a lot of inter-keyword dependencies, which we'd have to exhaustively define - -### [option 5] - -* Good, because it supports all use cases -* Good, because it's a familiar syntax -* Bad, because properties are not naturally grouped together -* Bad, because it's very verbose -* Bad, because `ifProperties` is very niche. Will this spawn a new series of `if*` keywords? How would it interact with `if`? - -### [option 6] - -* Good, because it supports all use cases -* Bad, because it's an unintuitive syntax and easy to get wrong -* Bad, because properties are not naturally grouped together - -## Links - -* [Link type] [Link to ADR] -* … - - ## [Appendix] Problems With Existing Patterns {#problems} ### `oneOf`/`anyOf` From a48fd8d5247e9f3e5300d0febe19f88d9f0dd4a6 Mon Sep 17 00:00:00 2001 From: Greg Dennis Date: Wed, 15 May 2024 10:07:43 +1200 Subject: [PATCH 12/12] remove redundant 'problems' appendix and link to adr; add status section --- proposals/propertyDependencies-adr.md | 2 +- proposals/propertyDependencies.md | 79 +++++---------------------- 2 files changed, 14 insertions(+), 67 deletions(-) diff --git a/proposals/propertyDependencies-adr.md b/proposals/propertyDependencies-adr.md index cb30be46..315357b6 100644 --- a/proposals/propertyDependencies-adr.md +++ b/proposals/propertyDependencies-adr.md @@ -7,7 +7,7 @@ Technical Story: - Issue discussing feature - https://github.com/json-schema-org/json-schema-spec/issues/1082 -- PR to add to the spec - https://github.com/json-schema-org/json-schema-spec/issues/1082 +- PR to add to the spec - https://github.com/json-schema-org/json-schema-spec/pull/1143 - ADR to extract from the spec and use feature life cycle - https://github.com/json-schema-org/json-schema-spec/pull/1505 ## Context and Problem Statement diff --git a/proposals/propertyDependencies.md b/proposals/propertyDependencies.md index 850696bc..757b5652 100644 --- a/proposals/propertyDependencies.md +++ b/proposals/propertyDependencies.md @@ -6,6 +6,15 @@ The `propertyDependencies` keyword is a more friendly way to select between two or more schemas to validate an instance against than is currently supported by JSON Schema. +## Current Status + +This proposal is complete and awaiting integration into the specification. + +As some additional context, this proposal has been written prior to the stable +specification's initial release. As such, it will not be integrated until at +least the spec's second release at the earliest. It is also operating as a +proving grounds, of sorts, for the SDLC's Feature Life Cycle. + ## Note to Readers The issues list for this document can be found at @@ -30,7 +39,7 @@ specification](../jsonschema-core.html) also apply to this document. A common need in JSON Schema is to select between one schema or another to validate an instance based on the value of some property in the JSON instance. There are a several patterns people use to accomplish this, but they all have -significant [problems](#problems). +significant [problems](propertyDependencies-adr.md#problems). OpenAPI solves this problem with the `discriminator` keyword. However, their approach is more oriented toward code generation concerns, is poorly specified @@ -113,73 +122,11 @@ subsection of "Keywords for Applying Subschemas Conditionally". } ``` -## [Appendix] Problems With Existing Patterns {#problems} - -### `oneOf`/`anyOf` - -The pattern of using `oneOf` to describe a choice between two schemas has become -ubiquitous. - -```jsonschema -{ - "oneOf": [ - { "$ref": "#/$defs/aaa" }, - { "$ref": "#/$defs/bbb" } - ] -} -``` - -However, this pattern has several shortcomings. The main problem is that it -tends to produce confusing error messages. Some implementations employ -heuristics to guess the user's intent and provide better messaging, but that's -not wide-spread or consistent behavior, nor is it expected or required from -implementations. - -This pattern is also inefficient. Generally, there is a single value in the -object that determines which alternative to chose, but the `oneOf` pattern has -no way to specify what that value is and therefore needs to evaluate the entire -schema. This is made worse in that every alternative needs to be fully validated -to ensure that only one of the alternative passes and all the others fail. This -last problem can be avoided by using `anyOf` instead, but that pattern is much -less used. - -### `if`/`then` - -We can describe this kind of constraint more efficiently and with with better -error messaging by using `if`/`then`. This allows the user to explicitly specify -the constraint to be used to select which alternative the schema should be used -to validate the schema. However, this pattern has problems of it's own. It's -verbose, error prone, and not particularly intuitive, which leads most people to -avoid it. - -```jsonschema -{ - "allOf": [ - { - "if": { - "properties": { - "foo": { "const": "aaa" } - }, - "required": ["foo"] - }, - "then": { "$ref": "#/$defs/foo-aaa" } - }, - { - "if": { - "properties": { - "foo": { "const": "bbb" } - }, - "required": ["foo"] - }, - "then": { "$ref": "#/$defs/foo-bbb" } - } - ] -} -``` - ## [Appendix] Change Log -* [October 2023] Created +* [March 2021] - Initially proposed +* [October 2021] Added to specification document +* [May 2024] Extracted from specification document as experimental feature ## Champions