Skip to content

Validators

Validators provide contextual error messages on input widgets. Each validator validates a particular shape of data — string, number, boolean, array — and supports a set of rules tailored to that shape. You can also create custom validators for application-specific rules.

A validator is just a validator object on an input widget, with the rules for that input’s value shape plus an optional messages block. The shape depends on how much the widget already knows about the value:

  • DX, typed-value inputstextInput, numberInput, booleanInput, password, currency, textarea, markdown, checkbox, calendar, dateInput, datePicker, rangeCalendar, rangeDateInput, rangeDatePicker, repeater. The widget pins the value type (a textInput is always a string, a numberInput is always a number, etc.), so the validator’s type is inferred. Write only the rules: validator: { minLength: 8, ... }.
  • DX, polymorphic inputsdropdown, radiogroup, select, list, and any gui.inputs.custom. The value type depends on the items or component you wire up, so the widget can’t pin it. The validator carries an explicit type: discriminator: validator: { type: 'string', required: true }.
  • JSON — the validator always carries an explicit type: discriminator: validator: { type: 'string', minLength: 8, ... }. The JSON path has no type inference; what you write is what the engine validates.

The rest of this page uses DX typed-value inputs in its examples; the JSON tab next to each shows the shape with the discriminator. The rules and message keys are identical in all three cases — only the presence of type: changes.

A validator without a messages block falls back to Zod’s defaults — strings like “Invalid input” or “Required” that rarely tell the user what they should actually do. Try the demo: leave the field empty, type a short value, type a long value, type letters only:

Always pair every rule with a messages entry to give the user contextual, actionable feedback. The example below requires a non-empty password and supplies its own message:

import { gui } from '@golemui/gui-shared';
gui.inputs.textInput('user.password', {
validator: {
required: true,
messages: {
required: 'Please enter your password',
},
},
});
{
"kind": "input",
"type": "textinput",
"path": "user.password",
"validator": {
"type": "string",
"required": true,
"messages": {
"required": "Please enter your password"
}
}
}

Validators stack — combine multiple rules and you’ll see one message per failing rule. The next example checks the password is between 8 and 20 characters long, and that it contains both letters and numbers, with a tailored message for each rule:

gui.inputs.textInput('user.password', {
validator: {
required: true,
minLength: 8,
maxLength: 20,
pattern: '^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]+$',
messages: {
required: 'Please enter your password',
minLength: 'Password must be at least 8 characters',
maxLength: 'Password cannot exceed 20 characters',
pattern: 'Password must contain both letters and numbers',
invalid: 'Password must be plain text',
},
},
});
{
"kind": "input",
"type": "textinput",
"path": "user.password",
"validator": {
"type": "string",
"required": true,
"minLength": 8,
"maxLength": 20,
"pattern": "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]+$",
"messages": {
"required": "Please enter your password",
"minLength": "Password must be at least 8 characters",
"maxLength": "Password cannot exceed 20 characters",
"pattern": "Password must contain both letters and numbers",
"invalid": "Password must be plain text"
}
}
}

A messages entry can be a plain string (as above) or a Localizable shape — { key, default } — that the active translator resolves at render time. Pair this with the i18n feature and your validator messages localize alongside the rest of the form.

gui.inputs.textInput('user.password', {
validator: {
required: true,
messages: {
required: {
key: 'errors.password.required',
default: 'Please enter your password',
},
},
},
});
{
"kind": "input",
"type": "textinput",
"path": "user.password",
"validator": {
"type": "string",
"required": true,
"messages": {
"required": {
"key": "errors.password.required",
"default": "Please enter your password"
}
}
}
}

A more complete example combining several rule types — switch the language dropdown to see the same messages in English, Spanish, and French:

Each validator type supports a specific set of rules and message keys.

PropertyMessage keyDescription
(type check)invalidValue is not a string
requiredrequiredEmpty string when required: true
minLengthminLengthString length is below the minimum
maxLengthmaxLengthString length exceeds the maximum
patternpatternString does not match the regex pattern
formatformatString does not match the format (email, url, uuid, hostname, ipv4, ipv6, date, time, date-time, duration)
enumenumValue is not one of the allowed values
constconstValue does not match the exact value
PropertyMessage keyDescription
(type check)invalidValue is not a number
requiredrequiredEmpty string when required: true
minimumminimumValue is below the minimum
maximummaximumValue exceeds the maximum
exclusiveMinimumexclusiveMinimumValue is not greater than the exclusive minimum
exclusiveMaximumexclusiveMaximumValue is not less than the exclusive maximum
multipleOfmultipleOfValue is not a multiple of the specified number
enumenumValue is not one of the allowed values
constconstValue does not match the exact value
PropertyMessage keyDescription
(type check)invalidValue is not a boolean
requiredrequiredEmpty string when required: true
constconstValue does not match the expected boolean value
PropertyMessage keyDescription
(type check)invalidValue is not an array
requiredrequiredArray is empty when required: true
minItemsminItemsArray has fewer items than the minimum
maxItemsmaxItemsArray has more items than the maximum

The validateOn setting controls when field validation runs.

type ValidateOn =
| 'eager'
| 'change'
| 'blur'
| 'submit'
| ('change' | 'blur' | 'submit')[];

When not set, the default behaviour is 'eager'.

ModeTriggers when
'change'The user changes the field value
'blur'The user leaves the field
'submit'A submit event is emitted (all fields are touched first)
'eager'Any of the above happens (default)

Where validateOn lives on the form component depends on the path:

validateOn goes inside formConfig.

import { gui } from '@golemui/gui-shared';
import { GuiForm } from '@golemui/gui-react';
const config = {
formDef: [
gui.inputs.textInput('username', {
validator: {
required: true,
minLength: 2,
messages: {
required: 'Please enter a username',
minLength: 'Username must be at least 2 characters',
},
},
}),
gui.actions.button({ label: 'Create User', actionType: 'submit' }),
],
formConfig: { validateOn: 'change' as const },
};
export function MyForm() {
return <GuiForm config={config} />;
}

validateOn goes in the config object.

import { GuiForm } from '@golemui/gui-react';
import formDef from './my-form.json';
const config = { formDef, validateOn: 'change' as const };
export function MyForm() {
return <GuiForm config={config} />;
}