# react-hook-form + a Zod schema

zodResolver wires the schema to the form: Zod validates, react-hook-form routes errors per field, and z.infer supplies the value type.

> Page: https://ui.eneawork.it/blog/react-hook-form-zod-schema
> Markdown: https://ui.eneawork.it/blog/react-hook-form-zod-schema.md

*2026-11-01*

Collega Zod a react-hook-form con zodResolver: lo schema valida, il form distribuisce gli errori per campo e il tipo dei valori arriva da z.infer, senza dichiararlo due volte.

Il catalogo EneUI conta 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie, letto da /api/stats, e i due pacchetti dichiarano nel registro npm 1.639.036 byte (react-hook-form 7.91.0) e 6.148.944 byte (zod 4.6.7) non compressi, misurati il 1 novembre 2026. Le dimensioni vengono dal campo dist.unpackedSize e valgono per il pacchetto intero: il bundle finale è più piccolo.

## Chi valida cosa

Lo schema Zod contiene le regole, il resolver le traduce nel formato di react-hook-form, e il form decide quando eseguirle. Nessuno dei tre duplica la logica degli altri due.

import { z } from "zod"; export const signupSchema = z.object({ email: z.email(), password: z.string().min(12), }); export type SignupValues = z.infer;

## Il form in poche righe

const form = useForm({ resolver: zodResolver(signupSchema), defaultValues: { email: "", password: "" }, }); const onSubmit = form.handleSubmit(async (values) => { await fetch("/api/signup", { method: "POST", body: JSON.stringify(values), }); });

handleSubmit esegue prima il resolver: se lo schema fallisce, la funzione non parte e nessuna richiesta lascia il browser. Il corpo JSON che arriva al server è già stata controllato sul client, mai fidato.

## Errori per campo

{form.formState.errors.email && ( {form.formState.errors.email.message} )}

Il messaggio arriva dallo schema, l’attributo aria-invalid dallo stesso errore: campo e annuncio restano allineati senza codice di collegamento scritto a mano.

## Quando validare

## Il comando

npm install react-hook-form zod @hookform/resolvers

Le tre librerie sono distribuite con licenza MIT; il resolver è il ponte, quindi la versione di Zod deve stare nel range dichiarato da @hookform/resolvers.

## Record vivi nel catalogo

Tre record mostrano lo stesso schema di lavoro: multi-step-form valida un passo alla volta, sign-in tiene gli errori accanto ai campi, e la libreria shadcn fornisce i componenti del form.

## La limitazione

Il resolver gira solo nel browser: chi costruisce la richiesta a mano lo salta. Lo stesso schema va rieseguito sul server, e z.infer non protegge i valori predefiniti, che restano un oggetto scritto a mano e possono divergere dallo schema.

## Dove continuare

Lo schema da solo è in validazione con Zod; la versione con i componenti shadcn è in form shadcn con react-hook-form; il form a più passi è in form multi-step; il confronto fra le librerie è in librerie di form React.

## FAQ

### Perché serve un resolver?

Perché react-hook-form non conosce Zod: il resolver esegue lo schema e converte gli errori nella struttura che il form sa mostrare.

### Devo dichiarare il tipo due volte?

No. Il tipo viene da z.infer sullo schema; useForm lo riceve come parametro generico e i campi restano tipizzati.

### Dove va la validazione lato server?

Sul server, con lo stesso schema: il resolver del browser è comodità, non controllo. Il dato che entra va riletto e rivalidato.

### Posso validare a ogni tasto?

Sì, con mode onChange. Costa un’esecuzione dello schema per tasto, quindi tienilo per form piccoli o campi con regole semplici.

### Cosa succede se lo schema e i default non coincidono?

Il form compila lo stesso, ma un campo mancante resta vuoto e il tipo non lo segnala: i defaultValues vanno scritti a mano e tenuti allineati.
