Skip to content

Custom Validators

Custom validators are functions that follow the CustomValidatorSchemaFn interface. They take a configuration parameter and return a Zod schema.

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,
});
}
}),
);

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} />

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} />

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.

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.

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.

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', {
label: 'Confirm password',
validator: ({ $form }) => ({
type: 'custom',
passwordsMatch: $form.password ?? '',
}),
}),

Each time $form.password changes, the runtime function re-evaluates and the validator’s schema sees the new value.