zod2form
v2.1.3
Published
Your Zod schema is your form. No boilerplate.
Maintainers
Readme
zod2form
Your Zod schema is your React Form. No boilerplate.
Built on top of react-hook-form
Installation
npm install zod2form zodWorks with Zod 3 and Zod 4. Requires React >= 18.
Usage
1. Write your field components
zod2form does not ship any UI — you write your own components once and reuse them across every form. Each component receives a standard FieldProps payload:
// components.tsx
import type { FieldProps } from "zod2form"
export function TextInput({ label, error, isRequired, value, onChange, onBlur, name, placeholder }: FieldProps) {
return (
<label>
{label}{isRequired && " *"}
<input name={name} value={String(value ?? "")} onChange={onChange} onBlur={onBlur} placeholder={placeholder} />
{error && <span>{error}</span>}
</label>
)
}
export function Checkbox({ label, error, value, onChange, onBlur }: FieldProps) {
return (
<label>
<input type="checkbox" checked={!!value} onChange={e => onChange(e.target.checked)} onBlur={onBlur} />
{label}
{error && <span>{error}</span>}
</label>
)
}2. Register them
// fields.ts
import { defineFields } from "zod2form"
import { TextInput, Checkbox } from "./components"
export const f = defineFields({ text: TextInput, checkbox: Checkbox })defineFields() returns a typed form builder. Use f.string(), f.object(), etc. to build schemas — .field() autocompletes only your registered field types. Pass the same f as fields to <ZodForm />.
3. Define a schema
// schemas/contact.ts
import { f } from "../fields"
export const schema = f.object({
name: f.string().min(2).field("text").label("Full name").placeholder("Jan Kowalski"),
email: f.string().email().field("text").label("Email").placeholder("[email protected]"),
gdpr: f.boolean().refine(v => v === true, "Required").field("checkbox").label("I accept the privacy policy").default(false),
})4. Render the form
// contact-form.tsx
import { ZodForm } from "zod2form"
import { f } from "./fields"
import { schema } from "./schemas/contact"
export function ContactForm() {
return (
<ZodForm
schema={schema}
fields={f}
onSubmit={(data) => console.log(data)}
>
<button type="submit">Send</button>
</ZodForm>
)
}The builder forwards Zod's methods, so schema validation and parsing work with Zod 3 and Zod 4 methods such as .min(), .email(), .optional(), .refine(), .regex(), .url(), and .date(). You add form metadata with chains: .field("type"), .label("text"), .placeholder("text"), .hint("text"), .props({ ... }), .autoComplete("email"). Fields without .field() are not rendered.
ZodForm renders your registered components, wires up react-hook-form with the package's Zod 3/4 resolver, and handles validation. It scans the top-level schema.shape; nested objects, arrays, unions, and other complex values can still be validated by Zod, but are not automatically expanded into separate form controls. Empty required fields show "Required" automatically. Default mode is onTouched.
Error messages and translations
ZodForm uses short English defaults. Override individual messages with dict, provide a complete typed translation, or connect an i18next-style translator with t:
<ZodForm
schema={schema}
fields={f}
onSubmit={handleSubmit}
dict={{
required: "Required field",
"string.min": "Use at least {{minimum}} characters",
}}
>
<button type="submit">Send</button>
</ZodForm>Zamiast dict możesz przekazać t={(key, params) => i18next.t(\zod.${key}`, params)}`.
For complete control, pass errorMap={(issue) => ({ message: "..." })}. The same resolver also handles schemas created with either Zod 3 or Zod 4.
Example
See it running with real components, validation, and multiple form shapes:
The Schema tab shows executable schema code generated from the current example, including validations, defaults, enum values, field metadata, and select option values. The demo generator supports native Zod 3 and Zod 4 schema internals. A callback passed to .refine() on an already-created plain Zod 3 schema cannot be recovered from runtime introspection; the generator marks that case with an explicit warning instead of silently dropping it.
Advanced
react-hook-form access
ZodForm wraps your form in FormProvider. Use hooks from zod2form:
import { useZodFormContext, useWatch } from "zod2form"
function SubmitButton() {
const { formState } = useZodFormContext()
return <button disabled={formState.isSubmitting}>Send</button>
}
function LivePreview() {
const data = useWatch()
return <pre>{JSON.stringify(data, null, 2)}</pre>
}All react-hook-form hooks re-exported: useFormContext, useWatch, useFormState, useFieldArray, useController, FormProvider. useZodFormContext() adds fieldsMeta with schema metadata.
react-hook-form options
All useForm() options forwarded:
<ZodForm
schema={schema}
fields={f}
onSubmit={handleSubmit}
mode="onChange"
defaultValues={{ name: "Jan" }}
shouldFocusError={false}
/>Cheatsheet
| Export | What it does |
|---|---|
| defineFields() | Register field components, get a typed form builder |
| ZodForm | Render a form from schema + field registry |
| useZodFormContext() | useFormContext() + schema metadata |
| FieldProps | Props type for field components |
License
MIT
