Schema Validation (Zod, Yup, Valibot…)
Drive the whole form from one schema with GValidator.withSchema / withSchemaAsync. First-class support for any library implementing Standard Schema, with zero runtime dependencies.
gform-react validates everything on its own - native constraints,
custom rules, and async checks
cover every case without a schema library. Reach for withSchema only if you already have a
schema (e.g. one shared with your backend) and want it to be the single source of truth.
First-class Standard Schema support
GValidator.withSchema(schema) / withSchemaAsync(schema) wire a single
Standard Schema directly to the '*' validator. gform reads the
schema's ['~standard'] contract itself - no adapter, no runtime dependency - so any library
implementing it works out of the box: Zod ≥3.24, Valibot, ArkType (sync), Yup ≥1.7 (async), Joi ≥18.0.0.
Unlike routing each field to its own leaf sub-schema, gform parses the whole form object on
every validation pass, so object-level rules - .refine() / .superRefine(), conditional-required,
confirm-password - fire too, and each issue routes to the field named by its path:
import { z } from "zod";
import { GValidator, type GValidators } from "gform-react";
const signUpSchema = z
.object({
email: z.string().email("Enter a valid email"),
password: z.string().min(8, "At least 8 characters"),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: "Passwords must match",
path: ["confirm"], // routes the cross-field error onto the `confirm` field
});
const validators: GValidators<SignUpForm> = {
"*": new GValidator().withSchema(signUpSchema),
};Use '*' so the whole form is validated as one object - per-field entries would only see that
field's own value, which defeats cross-field rules.
Sync vs. async: withSchema vs. withSchemaAsync
withSchema- for schemas whose'~standard'.validate()returns synchronously (Zod, Valibot, ArkType). Runs inline, like a constraint check.withSchemaAsync- for schemas that return aPromise(Yup, or sync schemas with async refinements). Runs on the same debounced path aswithCustomValidationAsync(thedebounceprop onGInput, default 300 ms).
Passing an async schema to withSchema doesn't block submission correctly - in development it logs
a one-time warning telling you to switch to withSchemaAsync.
import { object, string, ref } from "yup";
import { GValidator, type GValidators } from "gform-react";
const signUpSchema = object({
email: string().email("Enter a valid email").required("Required"),
password: string().min(8, "At least 8 characters").required("Required"),
confirm: string().oneOf([ref("password")], "Passwords must match").required("Required"),
});
const validators: GValidators<SignUpForm> = {
"*": new GValidator().withSchemaAsync(signUpSchema),
};Each pass is cached by a signature of the current form values, so re-parsing only happens when a value actually changes - not on every keystroke across unrelated fields.
Keeping the other side of a pair in sync: validatorDeps
A schema issue updates the field its path points to - but only when that field validates.
In a confirm-password pair, fixing password should also re-check confirm even though the user
didn't touch it. Give the dependent field validatorDeps, listing the fields it should re-validate
against:
<GInput formKey="confirm" required validatorDeps={["password"]} />With this, editing password re-runs confirm's validator too, so a stale "Passwords must match"
clears as soon as the user fixes the other field.
Typing a shared schema
StandardSchemaV1 is exported for annotating a schema that's shared across your app (e.g. with a
server action), without committing to one schema library's own type:
import type { StandardSchemaV1 } from "gform-react";
export const signUpSchema: StandardSchemaV1<SignUpForm> = z.object({ /* ... */ });Try it live
Zod schema with a .refine() confirm-password rule, wired to '*' via withSchema. The confirm
field declares validatorDeps={["password"]} so it re-checks as password changes:
Delegating to a library without Standard Schema
If you use a validator that doesn't implement ['~standard'], you can still delegate per-field
inside withCustomValidation - map the result to gform's
true-means-error contract yourself:
const validators: GValidators<Form> = {
email: new GValidator().withCustomValidation((input) => {
const result = someValidator.check(input.value);
if (!result.ok) {
input.errorText = result.message;
return true; // invalid
}
return false; // valid
}),
};This loses the whole-object parsing withSchema gives you - object-level rules only see the one
field's value - so prefer withSchema/withSchemaAsync whenever the library implements Standard
Schema.
Next
- Custom Validation - the
withCustomValidationcontract in full, including manual cross-field rules. - Async Validation - the debounced async path
withSchemaAsyncruns on. - GValidator - execution order and the mutation gotcha.