Description
I can't figure out how to set up mutual exclusion of subset schemas elegantly.
I thought unevaluatedProperties
was the solution so that I can easily reject properties of the superset schema that don't appear in the subset schema, but I can't find the right pattern to set it up right.
Subset and Superset schemas
Let's define subset schema as such:
A schema is a subset schema of another schema if any instance of the first schema also is valid also in the second schema.
Similarly,
A schema is a superset schema of another schema if the other schema is a subset schema of it.
Rationale
I have a real world schema with 2 kinds of values. But the first option happens to be a subset of the second
option. I'd like to make it so that if the subset schema validates, the superset schema will not validate.
{
"foo": (Either subset schema or superset schema)
}
Attempt 1 - Use not
- PASS
oneOf:
- required: ['a']
properties:
a: {}
not:
required: ['b']
properties:
b: {}
- required: ['a', 'b']
properties:
a: {}
b: {}
This one works as expected, where {"a": true, "b": true}
only validates against the second of the oneOf options, but I have to nest the meat of the second option inside a not of the first. This is ugly, but effective. When I compose my schemas, I don't want to reference all possible superset schemas in the subset schema.
Attempt 2 - Use anyOf
- PASS
anyOf:
- required: ['a']
properties:
a: {}
- required: ['a', 'b']
properties:
a: {}
b: {}
This approach gives up on the "typing" quality of the oneOf where consumers of the instance can learn about the "type" of the schema by inspecting which branch validates. Consumers need more sophistication since both validate for {"a": true, "b": true}
.
Attempt 3 - Use unevaluatedProperties
at the top level - FAIL
unevaluatedProperties: false
oneOf:
- required: ['a']
properties:
a: {}
- required: ['a', 'b']
properties:
a: {}
b: {}
For this instance, {"a": true, "b": true}
, validation will fail on the oneOf
since both branches validate and the collected annotations from the valid oneOf branches make both "a"
and "b"
evaluated properties.
Attempt 4 - push unevaluatedProperties to child schemas - FAIL
If I push down unevaluatedProperties: false
, I get the same issue as we have with additionalProperties: false
, schemas can not be extended if a parent "tacks on" some more properties.
oneOf:
- unevaluatedProperties: false
required: ['a']
properties:
a: {}
- unevaluatedProperties: false
required: ['a', 'b']
properties:
a: {}
b: {}
properties:
c: {}
If I try this JSON, the second oneOf will fail on unevaluatedProperty
: "c"
{"a": true, "b": true, "c": true}
Attempt 5 - Introduce new in-place applicator firstOf
: PASS
Let's define a new in place applicator, firstOf
This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema.
An instance validates successfully against this keyword if it validates successfully against at least one schema defined by this keyword's value.
Annotations are only collected from the first valid sub-schema.
Consumers are to ignore subsequent valid subschemas beyond the first valid subschema
unevaluatedProperties: false
firstOf:
- required: ['a']
properties:
a: {}
- required: ['a', 'b']
properties:
a: {}
b: {}
Both branches validate this instance, {"a": true, "b": true}
, but only annotations from the first branch
are collected. This causes unevaluatedProperties
to disregard annotations from the second branch and thereby reject property "b"
as "unevaluated".