GValidator
The validator class - constructor inheritance, chainable (mutating) methods, constraint messages, custom and async rules, and dev-mode diagnostics.
GValidator builds the validation rules for one field. You create one per field and place it in the
validators map passed to GForm, keyed by formKey:
import { GValidator, type GValidators } from "gform-react";
const base = new GValidator().withRequiredMessage("Required");
const validators: GValidators<SignUpForm> = {
"*": base, // default for every field without its own entry
password: new GValidator(base).withMinLengthMessage("At least 8 characters"),
};This page is the class reference. How the validators map itself is wired - the "*" default, the
stable-reference requirement, and validatorKey resolution - lives on
GForm and
GInput → validatorKey. For validation
recipes - cross-field rules, async UX, Yup/Zod integration - see
Schema Validation.
Constructor and inheritance
new GValidator(base) copies the base validator's rules, so per-field validators can extend
a default instead of redefining it:
const base = new GValidator().withRequiredMessage("Required");
const validators: GValidators<SignUpForm> = {
"*": base,
password: new GValidator(base) // inherits the required rule…
.withMinLengthMessage("At least 8 characters"), // …and adds one
};The copy is a snapshot taken at construction: rules added to base after
new GValidator(base) do not propagate to the derived validator. Build base validators
completely before deriving from them.
Properties and methods
new GValidator(base?) is the only constructor; every other member is a with… method that
registers one rule and returns the same instance, so calls chain (and mutate it -
see below). Each message method accepts a string or a
function (input: GInputState) => string that receives the full field state - handy for
interpolating the constraint value or the key.
| Constraint message method | Pairs with GInput prop | Registers a message for |
|---|---|---|
withRequiredMessage | required | valueMissing |
withMinLengthMessage | minLength | tooShort |
withMaxLengthMessage | maxLength | tooLong |
withPatternMismatchMessage | pattern | patternMismatch |
withRangeUnderflowMessage | min | rangeUnderflow |
withRangeOverflowMessage | max | rangeOverflow |
withStepMismatchMessage | step | stepMismatch |
withTypeMismatchMessage | type (email/url/…) | typeMismatch |
withBadInputMessage | - | badInput |
| Rule method | Handler signature | Adds |
|---|---|---|
withCustomValidation | (input, fields) => boolean | RegExp | string | A synchronous custom rule (below). |
withCustomValidationAsync | (input, fields) => Promise<boolean> | An async custom rule, debounced (below). |
withSchema | (schema: StandardSchemaV1) => this | A whole-form rule from one sync Standard Schema (Zod / Valibot / ArkType). |
withSchemaAsync | (schema: StandardSchemaV1) => this | The async variant - Yup, or schemas with async refinements. |
The two withSchema* methods parse the whole form object per pass, so cross-field
.refine()/.superRefine() rules fire and route by each issue's path - see
Schema Validation for the full recipe.
type (email, url, tel, …) mainly exists to hint the right virtual keyboard on mobile -
browser enforcement of typeMismatch is inconsistent, so withTypeMismatchMessage alone is a
weak check. Prefer pattern + withPatternMismatchMessage (or a
custom/schema rule) for actual
validation, e.g. of an email address.
When a field carries both type and pattern, gform defers typeMismatch in favor of the
stricter patternMismatch, so withPatternMismatchMessage is the message that shows and the two
constraints never conflict. See
Constraint Validation for the full rule.
Chainable methods mutate the instance
Every with… method adds the rule to the instance it's called on and returns that same
instance - chaining is mutation, not copying. Calling a with… method on a shared validator
silently adds the rule to every field using it:
const base = new GValidator().withRequiredMessage("Required");
// ❌ mutates `base` - now EVERY "*" field has the minLength message
const validators: GValidators<SignUpForm> = {
"*": base,
password: base.withMinLengthMessage("At least 8 characters"),
};
// ✅ extend a copy
const validators: GValidators<SignUpForm> = {
"*": base,
password: new GValidator(base).withMinLengthMessage("At least 8 characters"),
};Custom rules: withCustomValidation
For anything the native API can't express. The handler receives the current input and all
fields, and follows an inverted contract:
new GValidator().withCustomValidation((input, fields) => {
if (input.value !== fields.password.value) {
input.errorText = "Passwords do not match"; // you set the message
return true; // ⚠️ true = ERROR
}
return false; // valid
});- Return
trueto signal an error - and setinput.errorTextyourself. - Return
false(or nothing) when the field is valid. - Returning a
RegExpor pattern string means "valid only if the value matches it".
Async rules: withCustomValidationAsync
Same contract, but the handler returns a Promise - for server-side checks like "is this
username taken?". Async runs are debounced per field (default 300 ms, configurable via
the debounce prop on GInput).
new GValidator().withCustomValidationAsync(async (input) => {
const taken = await checkUsernameTaken(input.value as string);
if (taken) {
input.errorText = "That username is taken";
return true;
}
return false;
});While an async check is in flight, the field is held in the error state (with an empty
errorText), so state.isInvalid stays true and a submit can't slip through mid-check. The
real result replaces it when the Promise resolves.
Execution order
When a field validates, its rules run in this order, stopping at the first failure - a field reports one error at a time:
- Constraint handlers, in registration order.
- Custom sync handlers, in registration order.
- Async handlers (debounced), sequentially - and only if everything synchronous passed.
Development warnings
In development builds, gform cross-checks validators against inputs and warns about mismatches (all of this is stripped from production builds):
- Duplicate Handlers - the same violation registered twice on one validator. This includes re-registering a violation the base validator already handles, since inherited rules count.
- Missing Prop - the validator has a message for a violation, but the input never declared
the matching constraint (e.g.
withMinLengthMessagewithoutminLengthon theGInput). - Missing Validator - the input declared a constraint, but no matching message method was registered, so a violation would block submission with no visible message.
Typing
GValidator<T> is generic over the form interface, typing the input/fields your handlers
receive. The map type ties it together:
type GValidators<T> = { [key in keyof T]?: GValidator<T> } & {
[key: string]: GValidator<T> | undefined; // "*" default + dynamic keys
};Try it live
A "*" base validator with a dynamic message, extended (correctly - via new GValidator(base))
for the password field. Submit empty to see the interpolated messages: