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. 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 methodPairs with GInput propRegisters a message for
withRequiredMessagerequiredvalueMissing
withMinLengthMessageminLengthtooShort
withMaxLengthMessagemaxLengthtooLong
withPatternMismatchMessagepatternpatternMismatch
withRangeUnderflowMessageminrangeUnderflow
withRangeOverflowMessagemaxrangeOverflow
withStepMismatchMessagestepstepMismatch
withTypeMismatchMessagetype (email/url/…)typeMismatch
withBadInputMessage-badInput
Rule methodHandler signatureAdds
withCustomValidation(input, fields) => boolean | RegExp | stringA synchronous custom rule (below).
withCustomValidationAsync(input, fields) => Promise<boolean>An async custom rule, debounced (below).
withSchema(schema: StandardSchemaV1) => thisA whole-form rule from one sync Standard Schema (Zod / Valibot / ArkType).
withSchemaAsync(schema: StandardSchemaV1) => thisThe 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 is a UX hint, not a validation strategy

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 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…