JSON schemas
A widget set has a second authoring surface besides its TypeScript namespace: JSON form
definitions. The schema layer behind that surface is layered the same way the
authoring pipeline is, so a widget set can publish its own schema tree instead of forking
the gui one. @golemui/schemas owns the shared core resource and the tooling;
@golemui/gui-schemas is the reference tree built on top of it.
Core owns structure, you own vocabulary
Section titled “Core owns structure, you own vocabulary”The split follows the same rule as the rest of the widget set architecture. Core publishes
exactly one $defs resource, common.schema.json, holding the structural pieces every
widget set needs: baseWidget, dotPath, uid, reactiveExpression, localizable,
chunkRef, include, exclude, boolOrWhen and on. Everything above that is yours,
including the validation vocabulary.
That last point is why validators.schema.json sits in the gui tree rather than in core. A
widget set that brings its own ValidatorFn describes its own config shape, and the only
cross-layer requirement is the entry pointer: the file must expose #/$defs/validator,
which is what the generated envelope refers to.
The published tree
Section titled “The published tree”The tree on golemui.com has one core directory and one directory per implementation:
schemas/ core/ common.schema.json shared structural $defs, the only core resource gui/ form.schema.json gui envelope (generated) widgets.schema.json gui widget union + knownWidgetTypes (generated) layout-widget.schema.json layout widget union (generated) validators.schema.json the standard validator set (handwritten) ranges.schema.json gui date and time range defs (handwritten) components/*.schema.json one schema per widget type (handwritten) core/ vendored copy of schemas/core (generated) form.schema.json legacy alias for gui/form.schema.json, kept foreverYour widget set publishes the same shape under its own base URL, its idBase, which does not
have to be on golemui.com. Each file’s $id is ${idBase} plus its path inside the tree, and
every cross-file reference is relative (./core/common.schema.json#/$defs/baseWidget).
JSON authoring then points $schema at your envelope:
{ "$schema": "https://example.com/schemas/kendo/form.schema.json", "states": { "review": "$form.review === true" }, "form": [{ "kind": "input", "type": "kendoText", "path": "customer.name" }], "autocomplete": "off"}form is the only required property and additionalProperties is false. Note that form is
an array: core also decodes a single root layout object there, but the schema does not
accept it, so JSON written against the schema always uses the array form. Core wraps it in the
synthetic flex root at initialization.
Reserved contracts in JSON
Section titled “Reserved contracts in JSON”Two of the reserved core contracts show up directly in JSON authoring:
flexis the type of the synthetic root core creates for a bare widget array, so your tree needs aflexcomponent schema even though authors rarely write it by hand.repeatercarries its template as a layout widget underprops.template, and the paths of widgets inside the template use the<repeaterPath>.items.<field>grammar. A repeater atguestsholds a text input atguests.items.name, and core rewrites that toguests.0.nameper item when it materializes the template. JSON authors write the grammar explicitly, so your component schemas must accept it.
Composition rules
Section titled “Composition rules”The tree uses plain $ref composition only. $dynamicRef would be the standard mechanism for
a base schema whose widget section each implementation fills in, but VS Code’s JSON language
service does not implement it, and editor support is the entire point of shipping a schema.
That is why the envelope and the widget union are generated per implementation rather than
inherited: the duplication is a template, not maintenance.
widgets.schema.json carries two things generated from one list, the formWidget union and
the knownWidgetTypes enum. Generating both together removes the failure mode where the enum
and the union drift apart, and the enum is what keeps the optional custom-widget fallback
disjoint from the built-in branches.
A widget type that exists only in the builder and has no JSON representation can be listed in
knownWidgetTypes without a schema branch. gui does this with renderer, whose props hold a
render function that JSON cannot express: using it in a JSON form is now rejected with a real
validation error instead of silently matching the custom fallback and failing at runtime.
The vendored core copy
Section titled “The vendored core copy”Every implementation tree contains a generated copy of the core file under its own directory,
for gui that is gui/core/common.schema.json. It is identical to its source apart from one
line, its $id, which is rebased onto the implementation tree.
The copy exists because relative refs cannot cross npm package boundaries. With it, the tree
resolves under both strategies that matter: absolute $id resolution (Ajv, editors fetching
published URLs) and document-relative file resolution (a consumer pointing into
node_modules). It is also what lets two widget sets load into one Ajv instance, since no two
files share an $id.
Loading the tree
Section titled “Loading the tree”In Ajv (the 2020 dialect), register every file under the tree keyed by its own $id, then
compile the envelope:
import Ajv2020 from 'ajv/dist/2020';
const ajv = new Ajv2020();for (const schema of everyFileInTheTree) { if (!ajv.schemas[schema.$id]) { ajv.addSchema(schema); }}const validate = ajv.compile({ $ref: 'https://example.com/schemas/kendo/form.schema.json',});Three rules make that work: register leaves before aggregates, dedupe through ajv.schemas,
and never call ajv.getSchema during registration. getSchema compiles on lookup, so calling
it while refs are still unregistered throws MissingRefError.
Editors behave differently. VS Code resolves each file’s relative refs against its absolute
$id, computes the published URLs and tries to download them, which fails offline, in
untrusted workspaces, and anywhere the idBase is not actually hosted. The answer is the
editor bundle, form.editor.schema.json: a generated, self-contained copy of everything
reachable from the envelope, with no $ids and only same-document #/$defs/... pointers.
Point a form file’s $schema at it during development, and regenerate after editing any
handwritten component or validators schema, because the bundle inlines those too.
See also
Section titled “See also”- Generating a schema tree: the manifest, the generator, and the scaffolder CLI
- Widget Sets overview: how the JSON half fits with the rest
- Framework form components: a JSON-only widget set is a form component without
resolveFormInput - Custom Widgets: the lighter path when the shipped set is enough