Custom Validators
Custom validators are functions that follow the CustomValidatorSchemaFn interface. They take a configuration parameter and return a Zod schema.
Implementing a custom validator
Section titled “Implementing a custom validator”import { z } from 'zod';import type { CustomValidatorSchemaFn } from '@golemui/gui-validators';
export const allowedNames: CustomValidatorSchemaFn = (names: string[]) => z.string().check( z.superRefine((val, ctx) => { if (val && !names.includes(val)) { ctx.addIssue({ code: 'custom', message: `Name "${val}" is not in ${names.map((n) => `"${n}"`).join(', ')}`, input: val, }); } }), );Registering it on the form
Section titled “Registering it on the form”The validator is referenced by name from a widget’s validator block. Where you register that name with the engine depends on the path:
In the Programmatic API, register custom validators inside formConfig.customValidators:
import { allowedNames } from './custom-validators/allowed-names';
const config = { formDef: [/* …your widgets… */], formConfig: { customValidators: { allowedNames } },};
// <GuiForm config={config} />import { allowedNames } from './custom-validators/allowed-names';
class MyFormApp { protected config = { formDef: [/* …your widgets… */], formConfig: { customValidators: { allowedNames } }, };}// template: <gui-form [config]="config"></gui-form>import { allowedNames } from './custom-validators/allowed-names';
export class FormElement extends LitElement { protected config = { formDef: [/* …your widgets… */], formConfig: { customValidators: { allowedNames } }, }; // render: html`<gui-form .config=${this.config}></gui-form>`}import { allowedNames } from './custom-validators/allowed-names';
const config = { formDef: [/* …your widgets… */], formConfig: { customValidators: { allowedNames } },};// <GuiForm :config="config" />import { allowedNames } from './custom-validators/allowed-names';
const form = document.getElementById('app-form');form.config = { formDef: [/* …your widgets… */], formConfig: { customValidators: { allowedNames } },};In the JSON path, custom validators are a top-level property of the config object — there is no formConfig wrapper for JSON forms.
import { allowedNames } from './custom-validators/allowed-names';import formDef from './my-form.json';
const config = { formDef, customValidators: { allowedNames },};
// <GuiForm config={config} />import { allowedNames } from './custom-validators/allowed-names';import formDef from './my-form.json';
class MyFormApp { protected config = { formDef, customValidators: { allowedNames }, };}// template: <gui-form [config]="config"></gui-form>import { allowedNames } from './custom-validators/allowed-names';import formDef from './my-form.json';
export class FormElement extends LitElement { config = { formDef, customValidators: { allowedNames }, }; // render: html`<gui-form .config=${this.config}></gui-form>`}import { allowedNames } from './custom-validators/allowed-names';import formDef from './my-form.json';
const config = { formDef, customValidators: { allowedNames },};// <GuiForm :config="config" />import { allowedNames } from './custom-validators/allowed-names';import formDef from './my-form.json';
const form = document.getElementById('app-form');form.config = { formDef, customValidators: { allowedNames },};Declaring the validator on a widget
Section titled “Declaring the validator on a widget”Use validator: { type: 'custom', <validatorName>: <config> }:
import { gui } from '@golemui/gui-shared';
gui.inputs.textInput('user.name', { validator: { type: 'custom', allowedNames: ['John', 'Jane'] },});{ "kind": "input", "type": "textinput", "path": "user.name", "validator": { "type": "custom", "allowedNames": ["John", "Jane"] }}The form engine resolves allowedNames against the registered map (formConfig.customValidators for Programmatic, config.customValidators for JSON) at validation time. Referencing a rule that isn’t registered fails loudly — the engine throws Unknown custom validator rule "…" listing the registered rules, instead of silently skipping the check.
Cross-field validation with runtime functions
Section titled “Cross-field validation with runtime functions”A CustomValidatorSchemaFn only sees the value of the field it’s attached to. To validate against another field — e.g. a confirmPassword that must equal an earlier password field — pair the custom validator with a runtime function on the widget. The runtime function reads $form and re-runs on every form-data change, feeding the live value of the other field into the validator’s config.
This combination is Programmatic-only: pure JSON forms cannot host a runtime function.
1. Implement the schema function
Section titled “1. Implement the schema function”import { z } from 'zod';import type { CustomValidatorSchemaFn } from '@golemui/gui-validators';
export const passwordsMatch: CustomValidatorSchemaFn = (other: string) => z.string().check( z.superRefine((val, ctx) => { if (val && val !== other) { ctx.addIssue({ code: 'custom', message: 'Passwords do not match', input: val, }); } }), );other is the live value of the other password field, fed in by the runtime function below.
2. Register it on the form
Section titled “2. Register it on the form”import { passwordsMatch } from './custom-validators/passwords-match';
const formConfig = { customValidators: { passwordsMatch },};3. Wire it on the widget with a runtime function
Section titled “3. Wire it on the widget with a runtime function”import { gui } from '@golemui/gui-shared';
gui.inputs.password('password', { label: 'Password' }),gui.inputs.password('confirmPassword', ({ $form }) => ({ label: 'Confirm password', validator: { type: 'custom', passwordsMatch: $form.password ?? '', },})),Each time $form.password changes, the runtime function re-evaluates and the validator’s schema sees the new value.
Injecting validation issues from event handlers
Section titled “Injecting validation issues from event handlers”Custom validators and runtime functions both run inside the engine. Sometimes the check has to happen in your application code instead — a rule that pure JSON forms can’t express declaratively, or a result that only arrives later, like a server telling you a username is taken. For those cases, an event handler can inject validation issues onto any widget by dispatching INJECT_VALIDATION_ISSUES through event.callback:
event.callback({ type: 'INJECT_VALIDATION_ISSUES', payload: { path: 'user.confirm', // the widget's data path issues: ['Passwords do not match'], // or null to clear },});Injected issues display like any validator error and make the form invalid until you clear them by injecting issues: null for the same path. They are not recalculated when data changes — your handler owns their lifecycle, so clear them on the happy path.
Because event handlers work identically for JSON and Programmatic forms, this is the way to get cross-field validation — like the passwords example above — into a pure JSON form, where runtime functions aren’t available.
Example: password confirmation in a JSON form
Section titled “Example: password confirmation in a JSON form”Both password widgets fire the same checkPasswordMatch event on change:
{ "form": [ { "kind": "input", "type": "password", "path": "user.password", "label": "Password", "on": { "change": "checkPasswordMatch" } }, { "kind": "input", "type": "password", "path": "user.confirm", "label": "Confirm password", "on": { "change": "checkPasswordMatch" } } ]}The application’s event-handler map compares the two values and injects — or clears — the issue on user.confirm:
import type { FormEvent } from '@golemui/core';
const eventHandlers = { checkPasswordMatch(event: FormEvent) { const { password, confirm } = ((event.data as any).user ?? {}) as { password?: string; confirm?: string; }; event.callback({ type: 'INJECT_VALIDATION_ISSUES', payload: { path: 'user.confirm', issues: confirm && confirm !== password ? ['Passwords do not match'] : null, }, }); },};See also
Section titled “See also”- Features / Validators — built-in validator types.
- Runtime functions — reading
$formfrom inside a widget definition. - Features / Events — wiring the event-handler map that
INJECT_VALIDATION_ISSUESdispatches from.