Skip to main content
zod-to-form · MIT · Zod v4
zod-to-formzod-to-form

Schema in. Form out.
It's your code.

This is not a form library. It's a code generator that reads your Zod schemas and gives you production-ready React forms — with shadcn/ui, React Hook Form, and full TypeScript inference. Use the runtime renderer to iterate, then eject to generated code you fully own.

Get StartedView on GitHubPlaygroundcoming soon

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.

Config
z2f.config.ts
// z2f.config.ts
import { defineConfig } from '@zod-to-form/cli';

export default defineConfig({
components: {
SignupForm: {
ui: 'shadcn',
mode: 'submit',
},
},
});
vite.config.ts
// 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(),
],
});
Code
src/App.tsx (original)
// 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)}
/>
);
}
Virtual module (compiled by plugin)
// 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.

SignupForm.tsx
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 + zodResolverz2f codegenSpeedupConfig
small (5 fields)502μs397μs1.26×codegen L1
medium (18 fields)1.53ms864μs1.77×codegen L2
large (50 fields)2.31ms1.68ms1.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.

Capabilityz2fAutoFormuniformsRJSF
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.

Zod v4
React Hook Form
shadcn/ui
@hookform/resolvers
TypeScript
Radix UI

Stop wiring forms by hand

Define the schema. Get the form. Keep full control.