Schemas and Diagnostics Reading Schemas Lesson 10
Discriminated and exclusive forms
Mental Model
Chapter 9 covered the basics of a form schema: keys, required vs optional, defaults, positional policy, and open forms. This chapter adds two structural patterns that plugins use to keep one head’s vocabulary manageable: a single form name with several variant shapes selected by a discriminant key, and forms that declare a bundle of keys as mutually exclusive. Both patterns produce their own family of diagnostic codes, and both interact with the keyword-pairing rule from chapter 5 in ways worth knowing before you read the error messages.
Discriminated Forms
Some forms reuse one head for several closely related shapes, gated by the value of one key. The schema doc will tell you two things about such a form:
- Discriminant - the gating key, e.g.
discriminant: kind. - Variants - one extra key set per allowed discriminant value.
An author-facing summary for a discriminated form looks like:
(track ...)
discriminant: kind
:name symbol required
:kind track-kind required ; member set [kick groove animation]
:from number optional
variant when kick
:step number required
:volume number optional
variant when groove
:pattern symbol required
:swing number optional
variant when animation
:mesh symbol optional
:parent symbol optional
Read it like this: :name, :kind, and :from are the common keys
- always allowed. The variant blocks list extra keys that are only
allowed when
:kindmatches the variant’swhenvalue. So(track :kind kick :step 4)is valid;(track :kind kick :mesh logo)is not -:meshonly exists under theanimationvariant.
Two author rules follow from the streaming validator:
- Set the discriminant first. Write
:kindbefore any variant-only key. A variant key encountered before the discriminant is set is reported asunknown_keywith a hint (“:kindmust be set before variant-only keys”), even if the key is valid for some variant. - The discriminant value must be in the closed member set. The
schema documents which values are allowed (here:
kick | groove | animation). Any other symbol getsnot_memberon the discriminant slot itself.
Worked example:
(track :kind kick :name k1 :step 4 :volume 0.7)
(track :kind animation :name a1 :mesh logo :parent root)
Both validate cleanly. The mental model: one form name, several shapes, picked by the discriminant.
Common breakage and repair:
(track :name k1 :step 4 :kind kick)
:step is encountered before :kind. Diagnostic: unknown_key on
:step with the discriminant-first hint. Repair by ordering the
discriminant first:
(track :kind kick :name k1 :step 4)
Cross-variant misuse:
(track :kind kick :name k1 :mesh logo)
:mesh is an animation variant key. Diagnostic: unknown_key -
:mesh is not allowed when :kind = kick. Repair either by changing
the discriminant or by removing the wrong-variant key:
(track :kind animation :name k1 :mesh logo)
Discriminant absent:
(track :name k1 :from 0)
Diagnostic: missing_discriminant_key. The validator skips the
variant required-key sweep in this case - it does not pile on
missing_required_key :step etc. Repair by adding :kind:
(track :kind kick :name k1 :step 4 :from 0)
Exclusive Groups (Exactly-One-Of)
Some forms accept several keys but only allow one of them at a time. A plugin can declare this directly with an exclusive group — the schema doc names the group, lists its alternatives, and tags it with one of two cardinalities:
exactly-one— exactly one alternative must be present. Omitting all and supplying more than one are both errors.at-most-one— zero or one. Omission is fine; supplying two or more is an error.
An author-facing summary looks like:
(phrase ...)
:name symbol required
:notes vector optional
:events vector optional
exclusive-group cardinality exactly-one
alt :notes
alt :events
Read it like this: :notes and :events are individually optional
(either column may be absent on its own), but the group rule says
that, taken together, exactly one must be present. The plugin is
saying “a phrase is either a note list or an event list — never
both, never neither.”
Worked example:
(phrase :name p0 :notes [E4 G4 A4 G4])
(phrase :name p1 :events [(n E4 0.5b) (rest 0.25b)])
Both validate cleanly: each phrase carries one alternative.
Common breakage and repair:
(phrase :name p2 :notes [E4 G4] :events [(n A4 0.5b)])
Diagnostic: mutually_exclusive_keys_present. The message lists
the alternatives (:notes | :events). Repair by removing one:
(phrase :name p2 :notes [E4 G4])
Required-one-of missing:
(phrase :name p3)
Diagnostic: required_one_of_missing. The group’s cardinality is
exactly-one, so omitting both alternatives is rejected. Repair by
adding one:
(phrase :name p3 :notes [E4])
Two author rules follow:
- The group is the source of truth for presence, not the keys’
optionalflag. Even if the schema marks both keysoptional false, a key participating in anexactly-onegroup will not produce its ownmissing_required_key— the group’srequired_one_of_missingcovers it. You see one diagnostic per root cause, not two. - Open forms bypass the group sweep. If the plugin marks the
form
open: true, exclusive groups do not fire — not for “both present” and not for “neither present.” Open forms are bags, and group cardinality is a closed-shape rule. If a plugin needs both extension metadata and an exclusive group, make the governed form closed and put free-form metadata in a separate open child form.
A form may declare more than one exclusive group, and a variant inside a discriminated form may declare its own groups. The same key cannot appear in two groups on the same scope (the plugin author would get a manifest-load error). As an author you only need to read the group blocks the schema documents — the rules above apply identically per group.
Multi-key bundles. An alternative may name more than one key; a multi-key bundle is “present” iff every key in the bundle is present on the form. The wire shape looks like:
(route ...)
:name symbol required
:from symbol optional
:to symbol optional
:at symbol optional
exclusive-group cardinality exactly-one
alt :keys [from to] ; bundle: both must be present together
alt :keys [at] ; bundle: just `at`
Read it as: (route :from a :to b) validates clean (the [from to]
bundle is fully present); (route :at c) also validates clean. Both
of these are errors:
(route :name r0 :from a) ; partial bundle — :to is missing
(route :name r1 :from a :to b :at c) ; both bundles present
The first fires exclusive_bundle_partial because :from is set but
:to is missing; the second fires
mutually_exclusive_keys_present. Either way, the diagnostic message
lists each bundle with +-joined keys (:from+:to | :at) so the
source of the rule is obvious.
Multi-key bundles round-trip through sjon export-schema as
{required: [<bundle-keys>]} entries inside oneOf. JSON Schema’s
required keyword is all-or-nothing per the standard, so partial
bundles fail validation as oneOf mismatch — they don’t produce a
“partial bundle” message at the JSON Schema layer. Use the SJON
validator (or the IR’s warning list) for the more specific
diagnostic.
Exercises
Discriminated-form drill. Use the (track ...) summary above:
(track :kind groove :name g1 :step 4)
:step is a kick-variant key, not a groove key. Repair to a
groove-variant shape:
(track :kind groove :name g1 :pattern straight :swing 0.1)
Discriminant ordering drill:
(track :name a1 :mesh logo :kind animation)
Repair by writing the discriminant first:
(track :kind animation :name a1 :mesh logo)
Exclusive-group drill. Use the (phrase ...) summary above:
(phrase :name p4 :notes [E4 G4] :events [(n A4 0.5b)])
Both alternatives are present. Repair by keeping one:
(phrase :name p4 :notes [E4 G4])
Empty-of-required drill:
(phrase :name p5)
Neither alternative is present and the group is exactly-one.
Repair by supplying one:
(phrase :name p5 :events [(n A4 0.5b)])
Mastery Check
-
On a discriminated form, why must the discriminant key appear before variant-only keys?
-
For an
exactly-oneexclusive group, what distinguishes the "both present" diagnostic from the "neither present" one? -
What does a partial multi-key exclusive bundle produce?
-
What happens if an
open: trueform declares an exclusive group?