{"action":"created","issue":{"url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/64","repository_url":"https://api.github.com/repos/json-schema-org/json-schema-spec","labels_url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/64/labels{/name}","comments_url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/64/comments","events_url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/64/events","html_url":"https://github.com/json-schema-org/json-schema-spec/issues/64","id":177705404,"number":64,"title":"v6: conditional selection of alternate schemas (includes \"switch\" and other options)","user":{"login":"handrews","id":2358015,"avatar_url":"https://avatars.githubusercontent.com/u/2358015?v=3","gravatar_id":"","url":"https://api.github.com/users/handrews","html_url":"https://github.com/handrews","followers_url":"https://api.github.com/users/handrews/followers","following_url":"https://api.github.com/users/handrews/following{/other_user}","gists_url":"https://api.github.com/users/handrews/gists{/gist_id}","starred_url":"https://api.github.com/users/handrews/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/handrews/subscriptions","organizations_url":"https://api.github.com/users/handrews/orgs","repos_url":"https://api.github.com/users/handrews/repos","events_url":"https://api.github.com/users/handrews/events{/privacy}","received_events_url":"https://api.github.com/users/handrews/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":3,"created_at":"2016-09-19T05:17:00Z","updated_at":"2016-10-28T21:04:22Z","closed_at":null,"body":"### The Problem (and current workarounds)\r\n\r\nA common use case is to select an overall validation schema (or schemas) based on how a small subset of the instance validates.  In the simplest case, some property in the instance is checked against a set of literal values, and the overall validation schema(s) are chosen based on that literal value.  This is generally implemented with `oneOf` (or `anyOf`) and `enum` (although see also the `constant` proposal in issue #58 ).  Some forms of this problem may also be solved with `dependencies`.\r\n \r\n**Note:** Throughout this proposal, the elements of the `oneOf`/`anyOf` lists are referred to as *branches*, as in this case they are being used as implicit (or in some options, explicit) conditionals.\r\n\r\n#### Single selection with `oneOf`\r\n\r\nThis can be read as \"if foo is firstValue, bar must be present and must be a list of number, otherwise if foo is secondValue, buzz must be present and a string that is at least 10 characters long\":\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"oneOf\": [\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\"enum\": [\"firstValue\"]},\r\n                \"bar\": {\r\n                    \"type\": \"array\",\r\n                    \"items\": {\"type\": \"number\"}\r\n                }\r\n            },\r\n            \"required\": [\"foo\", \"bar\"]\r\n        },\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\"enum\": [\"secondValue\"]},\r\n                \"buzz\": {\r\n                    \"type\": \"string\",\r\n                    \"minLength\": 10\r\n                },\r\n                \"required\": [\"foo\", \"buzz\"]\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\nOne difficulty with this approach is that the cause and effect are not clear.  This could just as easily be read as \"If bar is present as a list of numbers, foo must be set to firstValue.  Otherwise if buzz is present and a string of at least ten characters, foo must be set to secondValue.\"\r\n\r\nThis is both a strength and a weakness.  The `oneOf` construct an capture complex alternatives, but does not clearly express the idea of one part of the schema being the determining factor.  In a small schema like this, either interpretation is easy to read, and it’s easy to spot the enum and guess that it is probably the determinant.  In a more complex schema, where maybe there are two values of the enum that select one branch and one in the other, that is much less easy to spot.\r\n\r\n#### Multiple selection with `anyOf`\r\n\r\nHere is a similar example using `anyOf` with, where foo being set to multiSelect can potentially validate against two branches (although is only required to validate against one or the other).  If foo is set to singleSelect it must validate against the lone schema that accepts that value.  Multiple matches are prominently addressed in one of the proposals so we will use this example of consider the options.\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"required\": \"foo\",\r\n    \"anyOf\": [\r\n        {\"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}, \"bar\": {\"type\": \"number\"}}},\r\n        {\"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}, \"buzz\": {\"type\": \"string\"}}},\r\n        {\"properties\": {\"foo\": {\"enum\": [\"singleSelect\"]}, \"zippy\": {\"type\": \"boolean\"}}},\r\n    ]\r\n}\r\n```\r\n\r\nIn order to *require* validation against *both* \"multiSelect\"-designated schemas, an `allOf` must be introduced:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"anyOf\": [\r\n        {\r\n            \"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}},\r\n            \"allOf\": [\r\n                {\"properties\": {\"bar\": {\"type\": \"number\"}}},\r\n                {\"properties\": {\"buzz\": {\"type\": \"string\"}}}\r\n            ]\r\n        }\r\n        {\"properties\": {\"foo\": {\"enum\": [\"singleSelect\"]}, \"zippy\": {\"type\": \"boolean\"}}},\r\n    ]\r\n}\r\n```\r\n\r\n#### Single selection with `dependencies`\r\n\r\nThis schema validates the same set of instances as the schema above that uses `oneOf`:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"properties\": {\r\n        \"bar\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\"type\": \"number\"}\r\n        },\r\n        \"buzz\": {\r\n            \"type\": \"string\",\r\n        }\r\n    },\r\n    \"dependencies\": {\r\n        \"bar\": {\r\n            \"properties\": {\"foo\": {\"enum\": [\"firstValue\"]}},\r\n            \"required\": [\"foo\", \"bar\"]\r\n        },\r\n        \"buzz\": {\r\n            \"properties\": {\"foo\": {\"enum\": [\"secondValue\"]}},\r\n            \"required\": [\"foo\", \"buzz\"]\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nNote that `dependencies` can only specify things based on the presence or absence of properties, so the \"if bar is present, else if buzz is present\" interpretation *must* be used for this approach.  In some cases, that is exactly what needs to be expressed, but it seems to be more common to use a value as the determinant rather than the presence or absence of a particular property.\r\n\r\nIf the difference between foo being set to firstValue or secondValue was a difference in exactly how bar is validated (and buzz was not part of the schema at all), then the `oneOf` approach still works just fine, but the `dependencies` approach is impossible.\r\n\r\n#### Multiple selection with `dependencies`\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"properties\": {\r\n        \"bar\": {\"type\": \"number\"},\r\n        \"buzz\": {\"type\": \"string\"},\r\n        \"zippy\": {\"type\": \"boolean\"},\r\n    },\r\n    \"required\": [\"foo\"],\r\n    \"dependencies\": {\r\n        \"bar\": {\r\n            \"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}}\r\n        },\r\n        \"buzz\": {\r\n            \"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}}\r\n        },\r\n        \"zippy\": {\r\n            \"properties\": {\"foo\": {\"enum\": [\"singleSelect\"]}}\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nAgain, the logic is inverted from the most intuitive reading, with the presence or absence of the other properties determining the value of foo.  Since (in this multi-select example) \"foo\" is the only required property, it’s just about possible to make out the intention that \"foo\"’s value determines how \"bar\", \"buzz\", or \"zippy\" is validated.  But it is arguably substantially less clear than the `anyOf` example, and as with the single selection example, `dependencies` cannot handle selection based purely on a value.\r\n\r\n### The proposals\r\n\r\nThere are two possible approaches, one of which has two variants:\r\n* A [`switch` validation keyword](https://github.com/json-schema/json-schema/wiki/switch-(v5-proposal)), more or less as seen in many programming languages.  Originally proposed by @geraintluff (with [additional discussion](https://github.com/json-schema/json-schema/issues/205) in the old repo).\r\n* An annotation keyword that clarifies the author’s intent without changing validation rules.  One form of this was proposed by @mrjj as [`bounding`](https://github.com/json-schema/json-schema/wiki/bounding-(v5-purposal)), and I am proposing an alternate syntax here.\r\n\r\n#### Clarifying intent with an annotation property\r\n\r\nThis approach does not change validation at all.  Rather, it adds one or two annotation properties that allow schema readers or documentation generators to understand the intent of the schema author for how branches are selected.\r\n\r\n##### `selectWith`: pointers from outside the branches\r\n\r\n`selectWith` is an annotation keyword that appears at the same level as a `oneOf` or `anyOf`.  It is either a single [Relative JSON Pointer](https://tools.ietf.org/html/draft-luff-relative-json-pointer-00) or a list of them.  The pointers indicate which properties (or array indicies, for that matter) are intended to determine which branch of the `oneOf` (or branches of the `anyOf`) is/are taken.\r\n\r\nThe values must allow Relative JSON Pointers (which include regular JSON Pointers) in order to allow a schema to be included in another schema as a child schema.  Otherwise, the pointer would need to always have the correct full path, severely limiting re-use capabilities.  The pointer is resolved with respect to the **instance** structure.\r\n\r\nAs an annotation property, `selectWith` cannot affect validation.  Setting it to point to a non-existent property is legal and does not produce an error (following the general principle that nonsensical schemas are valid).  Setting it to a property that will only exist on some branches is also possible and to be expected.  Unspecified but allowed instance properties/array elements by default have a blank schema, allowing anything.\r\n\r\nHere is our single-select example rewritten with `selectWith`:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"selectWith\": \"0/foo\",\r\n    \"oneOf\": [\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\"enum\": [\"firstValue\"]},\r\n                \"bar\": {\r\n                    \"type\": \"array\",\r\n                    \"items\": {\"type\": \"number\"}\r\n                }\r\n            },\r\n            \"required\": [\"foo\", \"bar\"]\r\n        },\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\"enum\": [\"secondValue\"]},\r\n                \"buzz\": {\r\n                    \"type\": \"string\",\r\n                    \"minLength\": 10\r\n                },\r\n                \"required\": [\"foo\", \"buzz\"]\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\nRecall that the pointer is relative to the **instance** structure, so \"0/foo\" so this reads \"the schema used to validate this instance property are the ones which determine which branch is taken.\r\n \r\nThe `selectWith` for the multi-select `anyOf` would be identical.\r\n\r\n##### `selector`: booleans within each branch\r\n\r\n`selector` is an alternative syntax directly derived from @mrjj’s `bounding` proposal (so called because it put bounds on what parts of the schema needed to be fully processed, and therefore constrained error reporting only to the most relevant branches of `oneOf`/`anyOf` constructs).\r\n\r\nInstead of one annotation keyword at the top, `selector` is a boolean annotation keyword that may appear anywhere within child schemas in a branch.  If `selector` is the effect is essentially the same as putting a pointer to that location in `selectWith`.\r\n\r\nThe only difference is that `selectWith` pointers are applied to all branches, while `selector` can be placed in different locations in different branches (and some branches my not have any `selector`).  However, since unspecified properties/array elements have a blank schema (allowing anything) by default, the end effect is the same.  The validation outcome remains unchanged no matter which proposal is used.\r\n\r\nHere is the single select example using `selector`:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"oneOf\": [\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\r\n                    \"enum\": [\"firstValue\"],\r\n                    \"bounding\": true\r\n                },\r\n                \"bar\": {\r\n                    \"type\": \"array\",\r\n                    \"items\": {\"type\": \"number\"}\r\n                }\r\n            },\r\n            \"required\": [\"foo\", \"bar\"]\r\n        },\r\n        {\r\n            \"properties\": {\r\n                \"foo\": {\r\n                    \"enum\": [\"secondValue\"],\r\n                    \"bounding\": true\r\n                },\r\n                \"buzz\": {\r\n                    \"type\": \"string\",\r\n                    \"minLength\": 10\r\n                },\r\n                \"required\": [\"foo\", \"buzz\"]\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\n@mrjj’s original purpose with `bounding` was to narrow the scope of validation and therefore produce more specific errors.  The approach is simply to validate anything marked `\"selector\": true` first, as anything that fails the selector validation will fail validation of the entire branch, so it is not necessary to proceed further (or report errors related to) that branch.\r\n\r\n##### `selectWith` and `selector` comparison\r\n\r\nWhile they may produce slightly different short-circuit validation behavior, neither of these change the validation outcome.\r\n\r\n`selector` appears within the schema doing the selecting, which makes its effect obvious as soon as you spot it.  On the other hand, spotting the selectors scattered throughout a complex set of branches is tedious and error-prone, and implementations will need to walk the branches and locate all of the selectors before being able to use them for short-circuit validation or anything else.\r\n\r\n`selectWith` requires a bit more interpretation for humans who may have to eyeball how a long JSON pointer actually lines up with the branches.  However, all selectors are gathered in one place and can be used as soon as they are encountered.\r\n\r\nIt would be possible to use both, for flexibility (which is why I gave them different names- something may be a `selector` for an outer `oneOf` while specifying a `selectWith` for an inner `oneOf`.  I feel like having both adds complexity without providing much gain.\r\n\r\nI am obviously biased, but I prefer `selectWith` simply because it reads much more clearly from the top down (assuming you put it above your branches).  It clearly says \"These fields are intended to determine which branch should validate.\"  You can then look across the branch schemas and see what the selection conditions are.  Which might be a bit tricky if the branches are complex, but no more so than trying to spot the `selector` keywords.\r\n\r\n`selectWith` also more closely matches how a documentation generator would use it- the documentation would reference it as part of the description of the whole branch set, so with `selector` it would have to collect them into essentially the `selectWith` value anyway.\r\n\r\n#### `switch`\r\n\r\n(much of this section’s wording is copied directly from @geraintluff)\r\n\r\nThe purpose of the `switch` keyword is to express a series of conditional relations: \"If A1 then B1, else if A2 then B2, else ...\".\r\n\r\n##### Values for `switch`\r\n\r\nThe value of `switch` is an array.  The entries in the array must be objects, each containing:\r\n\r\n* `then`: a schema or a boolean\r\n* *optional* `if`: a schema\r\n* *optional* `continue`: a boolean\r\n\r\n##### Validation of `switch`\r\n\r\nFor each object in the `switch` array:\r\n* if `if` is specified:\r\n  * if data is not valid against the schema in `if`, then continue to the next item in `switch`\r\n* if the value of `then` is a boolean:\r\n  * if the value of `then` is `false`, then validation fails\r\n* if the value of `then` is a schema:\r\n  * if the data is not valid against the schema in `then`, then validation fails\r\n* if `continue` is set to boolean `true`, then:\r\n  * continue to the next item in `switch`\r\n\r\n##### `switch` examples\r\n\r\nHere is our regular single-select implemented with `switch`:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"switch\": [\r\n        {\r\n            \"if\": {\"properties\": {\"foo\": {\"enum\": [\"firstValue\"]}}},\r\n            \"then\": {\r\n                \"properties\": {\r\n                    \"bar\": {\r\n                        \"type\": \"array\",\r\n                        \"items\": {\"type\": \"number\"}\r\n                    }\r\n                },\r\n                \"required\": [\"foo\", \"bar\"]\r\n            }\r\n        },\r\n        {\r\n            \"if\": {\"properties\": {\"foo\": {\"enum\": [\"secondValue\"]}}},\r\n            \"then\": {\r\n                \"properties\": {\r\n                    \"buzz\": {\r\n                        \"type\": \"string\",\r\n                        \"minLength\": 10\r\n                    },\r\n                    \"required\": [\"foo\", \"buzz\"]\r\n                }\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\nAnd here is our regular multi-select.  (Since nothing but foo is required and additional properties are allowed, it’s a bit silly to specify \"bar\" and \"buzz\" at separate schemas but pretend they are two schemas that make sense to do the sway because I don’t want to go redo all of the examples).\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"required\": \"foo\",\r\n    \"switch\": [\r\n        {\r\n            \"if\": {\"properties\": {\"foo\": {\"enum\": [\"multiSelect\"]}}},\r\n            \"then\": {\r\n                \"anyOf\": [\r\n                    {\"properties\": {\"bar\": {\"type\": \"number\"}}},\r\n                    {\"properties\": {\"buzz\": {\"type\": \"string\"}}}\r\n                ]\r\n            }\r\n        },\r\n        {\r\n            \"if\": {\"properties\": {\"foo\": {\"enum\": [\"singleSelect\"]}}},\r\n            \"then\": {\r\n                \"properties\": {\"zippy\": {\"type\": \"boolean\"}}\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\nThis actually isn’t very interesting because since the two branches associated with a foo of \"multiSelect\" are more concisely managed with an inner `anyOf`, the switch can once again only choose one of its conditions.  Here is a more complex example adapted from the original proposal:\r\n\r\n```JSON\r\n{\r\n    \"type\": \"object\",\r\n    \"switch\": [\r\n        {\r\n            \"if\": {\r\n                \"properties\": {\"indicator\": {\"enum\": [\"yellow\"]}}\r\n            },\r\n            \"then\": {\r\n                \"required\": [\"warningMessage\"]\r\n            },\r\n            \"continue\": true\r\n        },\r\n        {\r\n            \"if\": {\r\n                \"properties\": {\r\n                    \"powerLevel\": {\"minimum\": 9000}\r\n                }\r\n            },\r\n            \"then\": {\r\n                \"required\": [\"disbelief\"]\r\n            },\r\n        },\r\n        {\r\n            \"then\": {\r\n                \"required\": [\"confidence\"]\r\n            }\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\nIn this example, if there is a yellow indicator, there must also be a warning message.\r\nWhether there is a warning or not, a high enough `\"powerLevel\"` requires `\"disbelief\"`, otherwise it requires `\"confidence\".  \r\n\r\nSince the \"indicator\" branch specifies `\"continue\": true`, we go ahead and check the other conditions whether we have a yellow indicator or not.  And since continue is *not* specified on the \"powerLevel\" branch, if we match that condition we will *not* examine the remaining branches.\r\n\r\nBecause the last branch does not have an \"if\" schema, it will always match if we reach it.  So the only we we do not match it is if we match the minimum power level, as that will end the processing of the switch before we consider the final branch.\r\n\r\nAdditionally, the point of allowing `then` to be a boolean is to provide a concise expression to say that the data *must* be one of the supplied options, e.g.:\r\n```JSON\r\n{\r\n    \"switch\": [\r\n        {\"if\": ..., \"then\": ...},\r\n        {\"if\": ..., \"then\": ...},\r\n        {\"then\": false}\r\n    ]\r\n}\r\n```\r\n\r\n### Comparing the options\r\n\r\n`selectWith`/`selector`:\r\n\r\n* Geared towards documentation and schema readers\r\n* Clarifies schema author intent, but does not make the mental model of `anyOf`/`oneOf` any more intuitive\r\n* Does not change validation\r\n\r\n`switch`:\r\n\r\n* Adds a new validation approach\r\n* Geared towards schema writers and readers\r\n* More familiar model to many programmers\r\n* Introduces imperative conditionals to what has previously been a declarative system"},"comment":{"url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/comments/257028210","html_url":"https://github.com/json-schema-org/json-schema-spec/issues/64#issuecomment-257028210","issue_url":"https://api.github.com/repos/json-schema-org/json-schema-spec/issues/64","id":257028210,"user":{"login":"epoberezkin","id":2769109,"avatar_url":"https://avatars.githubusercontent.com/u/2769109?v=3","gravatar_id":"","url":"https://api.github.com/users/epoberezkin","html_url":"https://github.com/epoberezkin","followers_url":"https://api.github.com/users/epoberezkin/followers","following_url":"https://api.github.com/users/epoberezkin/following{/other_user}","gists_url":"https://api.github.com/users/epoberezkin/gists{/gist_id}","starred_url":"https://api.github.com/users/epoberezkin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/epoberezkin/subscriptions","organizations_url":"https://api.github.com/users/epoberezkin/orgs","repos_url":"https://api.github.com/users/epoberezkin/repos","events_url":"https://api.github.com/users/epoberezkin/events{/privacy}","received_events_url":"https://api.github.com/users/epoberezkin/received_events","type":"User","site_admin":false},"created_at":"2016-10-28T21:04:22Z","updated_at":"2016-10-28T21:04:22Z","body":"Actually the assumption that anyOf is sequential is not needed... They are equivalent."}}
{"actor":{"display_login":"epoberezkin"}}