Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract propertyDependencies #1506

Merged
merged 12 commits into from
May 22, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 37 additions & 5 deletions build/build.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 2 additions & 17 deletions jsonschema-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
"main": "index.js",
"scripts": {
"lint": "eslint build/",
"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-all": "node build/build.js jsonschema-*.md proposals/*.md",
"build": "node build/build.js"
},
"license": "MIT",
"dependencies": {
Expand Down
186 changes: 186 additions & 0 deletions proposals/propertyDependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# JSON Schema Proposal: The `propertyDependencies` Keyword

## Abstract

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.

## Note to Readers

The issues list for this document can be found at
<https://github.com/json-schema-org/json-schema-spec/issues?q=is%3Aissue+propertydependencies>.

For additional information, see <https://json-schema.org/>.

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](../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 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`.

### 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.

```json
{
"propertyDependencies": {
"foo": {
"aaa": { "$ref": "#/$defs/foo-aaa" }
}
}
}
```

The validation result is equivalent to the following schema.

```json
{
"if": {
"properties": {
"foo": { "const": "aaa" }
},
"required": ["foo"]
},
"then": { "$ref": "#/$defs/foo-aaa" }
}
```

### Limitations

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.
gregsdennis marked this conversation as resolved.
Show resolved Hide resolved

## Change Description

gregsdennis marked this conversation as resolved.
Show resolved Hide resolved
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/<version>/<release>/meta/applicator` at `/properties/propertyDependencies`:
```json
{
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"$dynamicRef": "#meta",
"default": true
},
"default": {}
}
}
```

## [Appendix] Problems With Existing Patterns {#problems}
gregsdennis marked this conversation as resolved.
Show resolved Hide resolved

### `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

## Champions

| Champion | Company | Email | URI |
|----------------------------|---------|----------------------|----------------------------------|
| Jason Desrosiers | Postman | <jdesrosi@gmail.com> | <https://github.com/jdesrosiers> |
Loading