|
| 1 | +# [short title of solved problem and solution] |
| 2 | + |
| 3 | +* Status: proposed |
| 4 | +* Deciders: @gregsdennis, @jdesrosiers, @relequestual |
| 5 | +* Date: 2022-04-07 |
| 6 | + |
| 7 | +Technical Story: |
| 8 | + |
| 9 | +- Issue discussing feature - https://github.com/json-schema-org/json-schema-spec/issues/1082 |
| 10 | +- PR to add to the spec - https://github.com/json-schema-org/json-schema-spec/issues/1082 |
| 11 | +- ADR to extract from the spec and use feature life cycle - https://github.com/json-schema-org/json-schema-spec/pull/1505 |
| 12 | + |
| 13 | +## Context and Problem Statement |
| 14 | + |
| 15 | +A common need in JSON Schema is to select between one schema or another to |
| 16 | +validate an instance based on the value of some property in the JSON instance. |
| 17 | +There are a several patterns people use to accomplish this, but they all have |
| 18 | +significant [problems](#problems). |
| 19 | + |
| 20 | +OpenAPI solves this problem with the `discriminator` keyword. However, their |
| 21 | +approach is more oriented toward code generation concerns, is poorly specified |
| 22 | +when it comes to validation, and is coupled to OpenAPI concepts that don't exist |
| 23 | +is JSON Schema. Therefore, it's necessary to define something new rather than |
| 24 | +adopt or redefine `discriminator`. |
| 25 | + |
| 26 | +## Decision Drivers |
| 27 | + |
| 28 | +- Ease of use |
| 29 | +- Readability |
| 30 | +- Coverage of most common use cases |
| 31 | +- Coverage of all use cases |
| 32 | +- Ease of implementation |
| 33 | + |
| 34 | +## Considered Options |
| 35 | + |
| 36 | +All of the following options have the same validation result as the following schema. |
| 37 | + |
| 38 | +```json |
| 39 | +{ |
| 40 | + "if": { |
| 41 | + "properties": { |
| 42 | + "foo": { "const": "aaa" } |
| 43 | + }, |
| 44 | + "required": ["foo"] |
| 45 | + }, |
| 46 | + "then": { "$ref": "#/$defs/foo-aaa" } |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | + |
| 51 | +### Option 1 |
| 52 | + |
| 53 | +The `dependentSchemas` keyword is very close to what is needed except it checks |
| 54 | +for the presence of a property rather than it's value. This option builds on |
| 55 | +that concept to solve this problem. |
| 56 | + |
| 57 | +```json |
| 58 | +{ |
| 59 | + "propertyDependencies": { |
| 60 | + "foo": { |
| 61 | + "aaa": { "$ref": "#/$defs/foo-aaa" } |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +### Option 2 |
| 68 | + |
| 69 | +This version uses an array of objects. Each object is a collection of the |
| 70 | +variables needed to express a property dependency. This doesn't fit the style of |
| 71 | +JSON Schema. There aren't any keywords remotely like this. It's also still too |
| 72 | +verbose. It's a little more intuitive than `if`/`then` and definitely less error |
| 73 | +prone. |
| 74 | + |
| 75 | +```jsonschema |
| 76 | +{ |
| 77 | + "propertyDependencies": [ |
| 78 | + { |
| 79 | + "propertyName": "foo", |
| 80 | + "propertySchema": { "const": "aaa" }, |
| 81 | + "apply": { "$ref": "#/$defs/foo-aaa" } |
| 82 | + }, |
| 83 | + { |
| 84 | + "propertyName": "foo", |
| 85 | + "propertySchema": { "const": "bbb" }, |
| 86 | + "apply": { "$ref": "#/$defs/foo-bbb" } |
| 87 | + } |
| 88 | + ] |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +### Option 3 |
| 93 | + |
| 94 | +A slight variation on that example is to make it a map of keyword to dependency |
| 95 | +object. It's still too verbose. |
| 96 | + |
| 97 | +```jsonschema |
| 98 | +{ |
| 99 | + "propertyDependencies": { |
| 100 | + "foo": [ |
| 101 | + { |
| 102 | + "propertySchema": { "const": "aaa" }, |
| 103 | + "apply": { "$ref": "#/$defs/foo-aaa" } |
| 104 | + }, |
| 105 | + { |
| 106 | + "propertySchema": { "const": "bbb" }, |
| 107 | + "apply": { "$ref": "#/$defs/foo-bbb" } |
| 108 | + } |
| 109 | + ] |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Option 4 |
| 115 | + |
| 116 | +This one is a little more consistent with the JSON Schema style (poor keyword |
| 117 | +naming aside), but otherwise has all the same problems as the other examples. |
| 118 | + |
| 119 | +```jsonschema |
| 120 | +{ |
| 121 | + "allOf": [ |
| 122 | + { |
| 123 | + "propertyDependencyName": "foo", |
| 124 | + "propertyDependencySchema": { "const": "aaa" }, |
| 125 | + "propertyDependencyApply": { "$ref": "#/$defs/foo-aaa" } |
| 126 | + }, |
| 127 | + { |
| 128 | + "propertyDependencyName": "foo", |
| 129 | + "propertyDependencySchema": { "const": "bbb" }, |
| 130 | + "propertyDependencyApply": { "$ref": "#/$defs/foo-bbb" } |
| 131 | + } |
| 132 | + ] |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +### Option 4 |
| 137 | + |
| 138 | +This one is a variation of `if` that combines `if`, `properties`, and `required` |
| 139 | +to reduce boilerplate. It's also essentially a variation of the previous example |
| 140 | +with better names. This avoids to error proneness problem, but it's still too |
| 141 | +verbose. |
| 142 | + |
| 143 | +```jsonschema |
| 144 | +{ |
| 145 | + "allOf": [ |
| 146 | + { |
| 147 | + "ifProperties": { |
| 148 | + "foo": { "const": "aaa" } |
| 149 | + }, |
| 150 | + "then": { "$ref": "#/$defs/foo-aaa" } |
| 151 | + }, |
| 152 | + { |
| 153 | + "ifProperties": { |
| 154 | + "foo": { "const": "bbb" } |
| 155 | + }, |
| 156 | + "then": { "$ref": "#/$defs/foo-aaa" } |
| 157 | + } |
| 158 | + ] |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +### Option 6 |
| 163 | + |
| 164 | +All of the previous alternatives use a schema as the discriminator. This |
| 165 | +alternative is a little less powerful in that it can only match on exact values, |
| 166 | +but it successfully addresses the problems we're concerned about with the |
| 167 | +current approaches. The only issue with this alternative is that it's not as |
| 168 | +intuitive as the chosen solution. |
| 169 | + |
| 170 | +```jsonschema |
| 171 | +{ |
| 172 | + "propertyDependencies": { |
| 173 | + "foo": [ |
| 174 | + ["aaa", { "$ref": "#/$defs/foo-aaa" }], |
| 175 | + ["bbb", { "$ref": "#/$defs/foo-bbb" }] |
| 176 | + ] |
| 177 | + } |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | + |
| 182 | +## Decision Outcome |
| 183 | + |
| 184 | +Option 1 was chosen because it satisfies the most common use cases while being |
| 185 | +sufficiently readable and easy to implement, even though it does not satisfy |
| 186 | +_all_ use cases, such as those where the property value is not a string. As |
| 187 | +these cases are significantly less common, the requirement to support all use |
| 188 | +cases carried a lower priority. |
| 189 | + |
| 190 | +### Positive Consequences <!-- optional --> |
| 191 | + |
| 192 | +- Some level of built-in support for a `discriminator`-like keyword that aligns |
| 193 | + with the existing operation of JSON Schema. |
| 194 | + |
| 195 | +### Negative Consequences <!-- optional --> |
| 196 | + |
| 197 | +- Properties with non-string values cannot be supported using this keyword and |
| 198 | + the `allOf`-`if`-`then` pattern must still be used. |
| 199 | + |
| 200 | +## Pros and Cons of the Options <!-- optional --> |
| 201 | + |
| 202 | +### [option 1] |
| 203 | + |
| 204 | +[example | description | pointer to more information | …] <!-- optional --> |
| 205 | + |
| 206 | +* Good, because it handle the most common use case: string property values |
| 207 | +* Good, because all property values are grouped together |
| 208 | +* Good, because it's less verbose |
| 209 | +* Bad, because it doesn't handle non-string property values |
| 210 | + |
| 211 | +### [option 2] |
| 212 | + |
| 213 | +* Good, because it supports all use cases |
| 214 | +* Bad, because properties are not naturally grouped together |
| 215 | +* Bad, because it's quite verbose |
| 216 | +* 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. |
| 217 | + |
| 218 | +### [option 3] |
| 219 | + |
| 220 | +* Good, because it supports all use cases |
| 221 | +* Good, because all property values are grouped together |
| 222 | +* Bad, because it's quite verbose |
| 223 | +* 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. |
| 224 | + |
| 225 | +### [option 4] |
| 226 | + |
| 227 | +* Good, because it supports all use cases |
| 228 | +* Bad, because properties are not naturally grouped together |
| 229 | +* Bad, because it's very verbose |
| 230 | +* Bad, because it introduces a lot of inter-keyword dependencies, which we'd have to exhaustively define |
| 231 | + |
| 232 | +### [option 5] |
| 233 | + |
| 234 | +* Good, because it supports all use cases |
| 235 | +* Good, because it's a familiar syntax |
| 236 | +* Bad, because properties are not naturally grouped together |
| 237 | +* Bad, because it's very verbose |
| 238 | +* Bad, because `ifProperties` is very niche. Will this spawn a new series of `if*` keywords? How would it interact with `if`? |
| 239 | + |
| 240 | +### [option 6] |
| 241 | + |
| 242 | +* Good, because it supports all use cases |
| 243 | +* Bad, because it's an unintuitive syntax and easy to get wrong |
| 244 | +* Bad, because properties are not naturally grouped together |
| 245 | + |
| 246 | +## Links <!-- optional --> |
| 247 | + |
| 248 | +* [Link type] [Link to ADR] <!-- example: Refined by [ADR-0005](0005-example.md) --> |
| 249 | +* … <!-- numbers of links can vary --> |
| 250 | + |
| 251 | + |
| 252 | +## [Appendix] Problems With Existing Patterns {#problems} |
| 253 | + |
| 254 | +### `oneOf`/`anyOf` |
| 255 | + |
| 256 | +The pattern of using `oneOf` to describe a choice between two schemas has become |
| 257 | +ubiquitous. |
| 258 | + |
| 259 | +```jsonschema |
| 260 | +{ |
| 261 | + "oneOf": [ |
| 262 | + { "$ref": "#/$defs/aaa" }, |
| 263 | + { "$ref": "#/$defs/bbb" } |
| 264 | + ] |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +However, this pattern has several shortcomings. The main problem is that it |
| 269 | +tends to produce confusing error messages. Some implementations employ |
| 270 | +heuristics to guess the user's intent and provide better messaging, but that's |
| 271 | +not wide-spread or consistent behavior, nor is it expected or required from |
| 272 | +implementations. |
| 273 | + |
| 274 | +This pattern is also inefficient. Generally, there is a single value in the |
| 275 | +object that determines which alternative to chose, but the `oneOf` pattern has |
| 276 | +no way to specify what that value is and therefore needs to evaluate the entire |
| 277 | +schema. This is made worse in that every alternative needs to be fully validated |
| 278 | +to ensure that only one of the alternative passes and all the others fail. This |
| 279 | +last problem can be avoided by using `anyOf` instead, but that pattern is much |
| 280 | +less used. |
| 281 | + |
| 282 | +### `if`/`then` |
| 283 | + |
| 284 | +We can describe this kind of constraint more efficiently and with with better |
| 285 | +error messaging by using `if`/`then`. This allows the user to explicitly specify |
| 286 | +the constraint to be used to select which alternative the schema should be used |
| 287 | +to validate the schema. However, this pattern has problems of it's own. It's |
| 288 | +verbose, error prone, and not particularly intuitive, which leads most people to |
| 289 | +avoid it. |
| 290 | + |
| 291 | +```jsonschema |
| 292 | +{ |
| 293 | + "allOf": [ |
| 294 | + { |
| 295 | + "if": { |
| 296 | + "properties": { |
| 297 | + "foo": { "const": "aaa" } |
| 298 | + }, |
| 299 | + "required": ["foo"] |
| 300 | + }, |
| 301 | + "then": { "$ref": "#/$defs/foo-aaa" } |
| 302 | + }, |
| 303 | + { |
| 304 | + "if": { |
| 305 | + "properties": { |
| 306 | + "foo": { "const": "bbb" } |
| 307 | + }, |
| 308 | + "required": ["foo"] |
| 309 | + }, |
| 310 | + "then": { "$ref": "#/$defs/foo-bbb" } |
| 311 | + } |
| 312 | + ] |
| 313 | +} |
| 314 | +``` |
0 commit comments