@exodus/schemasafe
supports a strict subset of the
discriminator
API.
It has the following additional requirements to be safe and compatible with upstream JSON Schema specification:
-
At least one of
oneOf
oranyOf
is required. Using both at the same time is not allowed. -
Type of the object using
discriminator
must be provable to beobject
— can be specified either astype
on the same level asdiscriminator
, or in each branch separately. -
Each
oneOf
/anyOf
branch must have aconst
orenum
rule on the property targeted bypropertyName
of thediscriminator
, either directly or inside a$ref
.While seeming a bit excessive, this is the rule that makes
discriminator
both well-defined and compatible with JSON Schema spec, while being a subset of OAPIdiscriminator
.Currently, those
const
orenum
values must be unique strings, which additionally makesoneOf
andanyOf
identical there in relation to the validation result. The uniqueness and being a string requirement might be lifted in the future, if it would be deemed useful. -
Property targeted by
propertyName
of thediscriminator
must be placed inrequired
, either on the same level asdiscriminator
, or in each branch separately. Failing to do so would make allconst
checks pass on that property, per the JSON Schema specification. -
mapping
is supported but only when it has the exact same set of branches asoneOf
/anyOf
, values of themapping
correspond to used$ref
values of the branches and the keys of the mapping matchconst
/enum
values on thepropertyName
of thediscriminator
in corresponding branches.That way,
mapping
doesn't really do anything at all, and brings in no new information to the validator. It only serves as a coherence check (@exodus/schemasafe
will refuse to compile a schema which has amapping
that mismatches the above constraints) and as an additional commentary to whoever is reading the schema.
Given these constraints, discriminator
is a provable noop in relation to the validation result.
The resulting constructions are 1:1 compatible with JSON Schema Spec — i.e. for each input, the
validation result stays the same with or without the discriminator
rule.
It affects three things though:
-
Error reporting — only errors related to the target
oneOf
/anyOf
branch would be reported (or a single error if doesn't match any), instead of errors from each branch merged together.Without
discriminator
in the sameoneOf
+const
/enum
combination, it might be very hard to understand from a large set of errors (of a first unrelated error) which exactly mismatched, if the validation failed. -
Optimization — the validator can optimize this efficiently, i.e. first just check the
const
orenum
value, then only check the corresponding branch of rules.This is also possible in
allErrors
mode due to filtering out errors from other branches.While this would have been doable without a
discriminator
rule, that affects the list of errors that are reported, and affecting that list without an explicit opt-in does not seem to be a good idea. -
Schema readability — this makes the schema more clear that just a
oneOf
/anyOf
over a set of branches withconst
/enum
rules.
All of the examples below are equivalent, i.e. pass and fail on the same input and produce a similar
list of errors, differing in just the keywordLocation
pointer because of refs.
{
"type": "object",
"required": ["objectType"],
"discriminator": { "propertyName": "objectType" },
"oneOf": [{
"properties": { "objectType": { "const": "obj1" } },
"required": ["a"]
}, {
"properties": { "objectType": { "const": "obj2" } },
"required": ["b"]
}]
}
{
"$defs": {
"obj1": {
"type": "object",
"properties": { "objectType": { "const": "obj1" } },
"required": ["objectType", "a"]
},
"obj2": {
"type": "object",
"properties": { "objectType": { "const": "obj2" } },
"required": ["objectType", "b"]
}
},
"discriminator": { "propertyName": "objectType" },
"oneOf": [
{ "$ref": "#/$defs/obj1" },
{ "$ref": "#/$defs/obj2" }
]
}
{
"$defs": {
"obj1": {
"type": "object",
"properties": { "objectType": { "const": "obj1" } },
"required": ["objectType", "a"]
},
"obj2": {
"type": "object",
"properties": { "objectType": { "const": "obj2" } },
"required": ["objectType", "b"]
}
},
"discriminator": {
"propertyName": "objectType",
"mapping": {
"obj1": "#/$defs/obj1",
"obj2": "#/$defs/obj2"
}
},
"oneOf": [
{ "$ref": "#/$defs/obj1" },
{ "$ref": "#/$defs/obj2" }
]
}
{
"$defs": {
"obj1": { "required": ["a"] },
"obj2": { "required": ["b"] }
},
"type": "object",
"required": ["objectType"],
"discriminator": { "propertyName": "objectType" },
"oneOf": [{
"properties": { "objectType": { "const": "obj1" } },
"$ref": "#/$defs/obj1"
}, {
"properties": { "objectType": { "const": "obj2" } },
"$ref": "#/$defs/obj2"
}]
}