One walker. Three integration paths.
Walk your Zod schema once. Pick the path that fits: render at runtime, generate static code with the CLI, or let the Vite plugin compile on demand. Same config, same output — zero runtime dependency on @zod-to-form/* in the code you ship.
// z2f.config.ts
import { defineConfig } from '@zod-to-form/cli';
export default defineConfig({
components: {
SignupForm: {
ui: 'shadcn',
mode: 'submit',
},
},
});
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import z2fVite from '@zod-to-form/vite';
export default defineConfig({
plugins: [
// Presence of `generate` opts in to JSX scanning:
// the plugin finds <ZodForm schema={X}/> call sites
// and compiles them away at build time.
z2fVite({ generate: {} }),
react(),
],
});
// src/App.tsx
import { ZodForm } from '@zod-to-form/react';
import { signupSchema } from './schemas/signup';
export default function App() {
return (
<ZodForm
schema={signupSchema}
onSubmit={(data) => console.log(data)}
/>
);
}
// Compiled on the fly by @zod-to-form/vite
// (virtual module — never hits disk, cached per schema)
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { signupSchema } from './schemas/signup.ts';
export default function Form(props) {
const form = useForm({
resolver: zodResolver(signupSchema),
});
return (
<form onSubmit={form.handleSubmit(props.onSubmit)}>
{/* <input>s generated from signupSchema … */}
</form>
);
}
Define your schema. Get a form.
No manual field wiring. Labels inferred, validation connected, types propagated to onSubmit.
import { z } from 'zod';
import { ZodForm } from '@zod-to-form/react';
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
});
export default function App() {
return (
<ZodForm
schema={schema}
onSubmit={(data) => console.log(data)}
/>
);
}
Faster than hand-wiring a form. By a lot.
We measure the full form lifecycle — mount + keystrokes + submit — against a hand-wired useForm + zodResolver baseline. The build-time walk eliminates per-mount optimizer cost, and native-rules mode bypasses Zod entirely for fields whose constraints can be expressed via minLength, pattern, min, max, and friends.
| Form size (20 edits) | Hand-wired + zodResolver | z2f codegen | Speedup | Config |
|---|---|---|---|---|
| small (5 fields) | 502μs | 397μs | 1.26× | codegen L1 |
| medium (18 fields) | 1.53ms | 864μs | 1.77× | codegen L2 |
| large (50 fields) | 2.31ms | 1.68ms | 1.37× | codegen L1 |
On heavy editing sessions (500 edits), the gap widens to 2.08× on medium forms and 2.00× on large forms — every keystroke in z2f costs ~100ns (a native-rule check) vs ~2.6μs (a full Zod parse) for the baseline. That's a 24× per-keystroke speedup on the hot path.
What sets zod-to-form apart
The Zod v4 form generation space has several players. None offer codegen, and none use the APIs Zod v4 designed for library authors.
| Capability | z2f | AutoForm | uniforms | RJSF |
|---|---|---|---|---|
| Zod v4 substrate API | ✓ | — | — | — |
| Build-time codegen | ✓ | — | — | — |
| Zero-dependency eject | ✓ | — | — | — |
| React Hook Form | ✓ | ✓ | — | — |
| shadcn/ui preset | ✓ | ✓ | — | — |
| Controlled component bridging | ✓ | — | — | — |
| Field template customization | ✓ | — | — | ✓ |
| Discriminated unions | ✓ | — | — | ✓ |
| Typed recursive config | ✓ | — | — | — |
Built on the standards
No custom runtime. Just Zod, React Hook Form, and your component library.
Stop wiring forms by hand
Define the schema. Get the form. Keep full control.