gform-react
Back to Classes

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. Instances live in the validators object you pass 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. For validation recipes - cross-field rules, async UX, Yup/Zod integration - see Schema Libraries.

The validators object

GValidators<T> maps each formKey to a GValidator. Two special lookups:

  • "*" - the default validator, used by every field that has no entry of its own.
  • A field resolves its validator as validators[validatorKey || formKey], falling back to "*" - so validatorKey on a GInput can point several dynamic fields at one shared entry.
Keep the object stable

Pass validators by a stable reference - declare it at module level or wrap it in useMemo. A new object on every render re-registers the fields.

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.

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

Constraint message methods

Each method registers the error message for one native constraint-validation violation. The constraint itself goes on the GInput; the validator supplies the message when the browser reports that violation:

MethodPairs with GInput propValidity violation
withRequiredMessagerequiredvalueMissing
withMinLengthMessageminLengthtooShort
withMaxLengthMessagemaxLengthtooLong
withPatternMismatchMessagepatternpatternMismatch
withRangeUnderflowMessageminrangeUnderflow
withRangeOverflowMessagemaxrangeOverflow
withStepMismatchMessagestepstepMismatch
withTypeMismatchMessagetype (email/url/…)typeMismatch
withBadInputMessage-badInput

Every method accepts a string or a function (input: GInputState) => string, which receives the full field state - useful for interpolating the constraint value or the key:

new GValidator()
  .withRequiredMessage((input) => `${input.formKey} is required`)
  .withMinLengthMessage((input) => `At least ${input.minLength} 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 true to signal an error - and set input.errorText yourself.
  • Return false (or nothing) when the field is valid.
  • Returning a RegExp or 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;
});
Pending means invalid

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:

  1. Constraint handlers, in registration order.
  2. Custom sync handlers, in registration order.
  3. 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. withMinLengthMessage without minLength on the GInput).
  • 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:

Loading playground…