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.

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/core';

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)
// Virtual module generated by @zod-to-form/vite
// (same output as CLI — 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 SignupForm(props) {
const form = useForm({
resolver: zodResolver(signupSchema),
});
return (
<form onSubmit={form.handleSubmit(props.onSubmit)}>
<input {...form.register('name')} />
<input {...form.register('email')} type="email" />
<select {...form.register('role')}>
<option>admin</option>
<option>editor</option>
<option>viewer</option>
</select>
<button type="submit">Submit</button>
</form>
);
}

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

Where does the gap come from? Session cost = fixed (mount + submit) + per-edit × K.

Mount + submit (fixed)
baseline ~700μsz2f ~700μs
1.0× equal startup cost
Per keystroke (hot path)
baseline ~2.6μsz2f ~100ns
26× native check vs full Zod parse
Session @ 500 edits (medium)
baseline ~2.0msz2f ~960μs
2.08× fixed cost dilutes

The per-keystroke gap is 26× — a native-rule check (minLength, pattern, …) vs a full Zod parse. Session speedup is always lower because both paths share the same fixed mount + submit cost. As edits scale (K=20 → K=500 → steady-state typing), the session ratio climbs from 1.77× toward the per-keystroke limit of 26×.

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.