Schema adapters: Zod and Standard Schema
Two ways to connect a validation schema to a Modyra form. Pick the first when you are all-in on Zod, the second for everything else.
@modyra/zod |
@modyra/standard-schema |
|
|---|---|---|
| Schema libraries | Zod ≥3.25 only | Any Standard Schema v1 vendor (Zod ≥3.24, Valibot, ArkType, …) |
| Field tree | Derived from z.object(...) — nested objects → groups, z.array() → typed field arrays, z.record() → keyed collections |
Declared by you with field()/group()/array()/record() |
Defaults / required |
Inferred from the schema | Declared in field(); schema defaults seed initials when validate({}) succeeds |
| Whole-schema rules | Object .refine() → cross-field validator |
Every issue → form error on its dotted path |
| Peer dependency | zod |
none |
Why the models differ
Section titled “Why the models differ”The Standard Schema spec standardizes
validation only: ~standard.validate(value) → { value } | { issues }.
There is no introspection API, so the adapter cannot discover fields from
the schema — it follows the same model as TanStack Form:
- you declare the field tree (initials, sync validators,
required); - the schema validates the whole form value; issues are attributed to
their dotted field path (
address.city,items.0.name), gateform.state.valid()and surface in the field’s error list; - schema defaults seed the declared initials when the whole schema
accepts
{}(all top-level fields optional/defaulted).
import * as v from "valibot";import { field } from "@modyra/core";import { createStandardForm, type MdyStandardSchemaTree } from "@modyra/standard-schema";
const schema = v.object({ email: v.pipe(v.string(), v.email("Invalid email")), age: v.pipe(v.number(), v.minValue(18, "18+ only")),});
// Opt-in: annotate with MdyStandardSchemaTree so schema/field drift// does not compile. Leaves are `Input | null` (null = not filled in): a form holds what a person// typed and what a server sent, and validates it against the schema — it does not run the schema's// transformations, so `z.coerce.number()` is a field holding `"42"`, not `42`.const fields: MdyStandardSchemaTree<typeof schema> = { email: field<string | null>(null), age: field<number | null>(18),};
const form = createStandardForm(schema, fields);The identical code works with a z.object(...) schema — the adapter test
suite runs against both Zod and Valibot and asserts identical results.
Async rules
Section titled “Async rules”Form-level validation is synchronous, so async schemas are rejected:
fully-async schemas throw at creation time, input-dependent async branches
hold the form invalid with a clear global error. Server-side checks belong
in the engine’s own async validation (serverValidator / field-level
asyncValidators), which adds cancellation, debounce and dependsOn
re-validation — see Typed forms.
Assembling it yourself
Section titled “Assembling it yourself”createStandardForm is the two pieces below, put together. Reach for them directly when you want
the tree or the validator on their own — to add your own form-level validators beside the schema’s,
or to hand the tree to a framework’s own form constructor (mdyForm, useMdyForm, createLitForm
…), which take the same shape.
import { createForm } from "@modyra/core";import { buildStandardTree, buildStandardValidator } from "@modyra/standard-schema";
const form = createForm(buildStandardTree(schema, fields), { validators: [buildStandardValidator(schema)],});The Zod adapter has the same pair: buildZodTree and buildZodRefinementValidator.
API summary
Section titled “API summary”createStandardForm(schema, fields, options?)— form from a declared tree + a Standard Schema.buildStandardTree(schema, fields)— declared tree with initials seeded from schema defaults.buildStandardValidator(schema)— schema as a form-level validator.MdyStandardSchemaTree<TSchema>— opt-in type-level agreement.
For the Zod adapter API see packages/zod; for field arrays see Typed forms.