Schema Validation
The second validation mechanism. See
Per-node Validation
for how it relates to validators. toSchemaValidator() (from
@kintools/form-validators) adapts any
Standard Schema-compliant library (zod v4+,
valibot v1+, arktype, ...) into a SchemaValidator that validates a whole
group's or form's value in one pass.
Pass it as schemaValidator, not validators, since it's a distinct option on
FieldApi/FormApi:
const checkoutSchema = z.object({
email: z.string().email(),
items: z.array(z.object({ code: z.string() })).min(1),
});
const form = new FormApi({
initialValue: { email: "", items: [] },
schemaValidator: toSchemaValidator(checkoutSchema),
});Reading the result
Running the schema populates schemaErrorMap, a flat dot-joined path -> message
map (e.g. { "email": "Invalid email", "items.0.code": "Required" }) built from
every issue's path. A field reads its own slice via field.error (which falls
back to it when the field has no own validator message), with no per-field
wiring needed, even through intermediate fields,
since the lookup walks up parent until it finds a map with an answer:
{
field.invalid && field.touched && <span>{field.error}</span>;
}An issue with no path (e.g. a schema-level .refine()) maps to the group's
own "" key — read via form.schemaErrorMap?.[""], or form.error, which
checks "" first before falling back to a parent's slice.
schemaErrorMap vs. error
schemaErrorMap is the raw path -> message map from the schema. A schema issue
is kept separate from the field's own validator message under the hood: nothing
is overwritten, so a field can carry both a hand-written validator message and a
schema issue at once. field.error is the one property to read either way: the
field's own message when it has one, its resolved slice of the schema result
otherwise.
Works the same nested or flat
One schema covers the whole subtree, so nested fields don't need
intermediate fields just to be addressed: a schema
on form validates contact.name or guests.0.email whether or not anything
called field("contact") first:
const form = useForm({
initialValue: { contact: { name: "", email: "" }, guests: [] },
schemaValidator: toSchemaValidator(registrationSchema),
});
// Flat — reads form's own schemaErrorMap directly.
<TextField api={form.field("contact.name")} label="Name" />;
// Nested — same resolved error, found by walking up through `contact`.
const contact = form.field("contact");
<TextField api={contact.field("name")} label="Name" />;Exception: if contact has its own schemaValidator, it takes precedence over
form's for everything under it. The nearer validator always wins, so
contact.name would read from contact's own map instead. An issue on the
array itself (e.g. Zod's .min(1, "Add at least one guest") on guests) has no
field at that exact path, so read it off the map directly:
form.schemaErrorMap?.guests. See
Flat vs. Nested Structure for when nesting is
still worth it.
Debouncing
Like asyncValidator, runs are debounced by validationDebounceMs (default
0), with the same debounce/coalescing/handleBlur-flush behavior as
described here.