Conditional Form Validation#
Formisch has no bespoke conditional-validation API. Instead, it delegates entirely to Valibot's schema composition primitives — variant, union, intersect, lazy, pick, omit, and partial — which are resolved at parse time when the form validates. The form root must output Record<string, unknown>, so every composition must ultimately produce an object . Any Valibot combinator schema that satisfies that structural constraint is accepted as a valid FormSchema .
The Payment Form Pattern#
The canonical reference for conditional validation is the payment form playground, implemented identically across all supported frameworks:
The schema uses v.intersect to combine a shared field (owner) with a v.variant that branches on a type discriminator :
const PaymentFormSchema = v.intersect([
v.object({ owner: v.pipe(v.string(), v.nonEmpty('...')) }),
v.variant('type', [
v.object({ type: v.literal('card'), card: v.object({ ... }) }),
v.object({ type: v.literal('paypal'), paypal: v.object({ ... }) }),
], 'Please select the payment type.'),
]);
The UI then reads the type field value via getInput and conditionally renders only the relevant branch fields . Validation errors for inactive branches are never surfaced because those fields are not rendered and thus never touched.
API Reference#
| API | Purpose | Valibot Docs |
|---|---|---|
v.variant(key, options, message?) | Discriminated union — uses a literal-typed key to select the matching branch. Preferred over union for discriminated shapes due to better performance and more targeted errors. | variant |
v.union(options, message?) | General OR — tries each schema in order; returns the first that succeeds. Use when there is no reliable discriminator key. | Unions |
v.intersect(options, message?) | AND / merge — all sub-schemas must pass; outputs are merged. Used to attach shared fields to a variant-branched core. | Intersections |
v.lazy(getter) | Deferred schema — evaluates the schema at parse time. Required for recursive/self-referential schemas. Not supported in Qwik . | lazy |
v.partial(schema, keys?) | Make entries optional — mirrors TypeScript's Partial<T>. Apply before pipe(), not after. | partial |
v.pick(schema, keys) | Include selected entries | Methods |
v.omit(schema, keys) | Exclude selected entries | Methods |
Runtime Internals#
When a form initializes, initializeFieldStore walks the schema tree. For intersect, union, and variant nodes it iterates all schema.options and recursively initializes one shared field store per path key . Because branches share a single store, per-branch metadata (e.g., whether a key is nullish in one branch vs. required in another) is approximated on a last-write-wins basis — see the comment in initializeFieldStore.ts for the known limitation and planned fix.
For lazy schemas, the getter is called with undefined to unwrap the schema at initialization time , except in Qwik where lazy schemas throw an unsupported error .
Validation itself runs via the form's stored parse function, which calls v.safeParseAsync against the full root schema; Valibot resolves the active branch at that point.
Accepted Root Schema Forms (Type Tests)#
The schema.test-d.ts file documents and enforces which schemas pass the FormSchema constraint:
- Accepted:
object,looseObject,strictObject,intersect([…objects]),union([…objects]),variant('key', […objects]),lazy(() => object/union/variant) - Rejected: primitives (
string,number),array(…),optional(object(…)),unionwith any non-object member,lazy(() => string())