Trasformazioni e Refine
.transform()
Il metodo .transform() permette di modificare i dati dopo la validazione. Il tipo di output sara’ diverso dal tipo di input.
import { z } from "zod";
// Stringa -> numero
const stringToNumber = z.string().transform((val) => parseInt(val, 10));
stringToNumber.parse("42"); // => 42 (number)
type Input = z.input<typeof stringToNumber>; // string
type Output = z.infer<typeof stringToNumber>; // number
Trasformazioni Comuni
// Stringa -> Date
const stringToDate = z.string().transform((val) => new Date(val));
// Stringa -> booleano
const stringToBool = z.string().transform((val) => val === "true");
// Stringa -> array
const csvToArray = z.string().transform((val) => val.split(","));
csvToArray.parse("a,b,c"); // => ["a", "b", "c"]
Chaining di Trasformazioni
Puoi concatenare piu’ trasformazioni in sequenza:
const schema = z.string()
.trim()
.toLowerCase()
.transform((val) => val.split(" "))
.transform((parole) => parole.map(p => p.charAt(0).toUpperCase() + p.slice(1)))
.transform((parole) => parole.join(" "));
schema.parse(" CIAO MONDO "); // => "Ciao Mondo"
.refine()
Aggiunge una validazione custom. Deve restituire true (valido) o false (non valido).
const password = z.string().refine(
(val) => val.length >= 8 && /[A-Z]/.test(val) && /[0-9]/.test(val),
{ message: "La password deve avere almeno 8 caratteri, una maiuscola e un numero" }
);
password.parse("Abc12345"); // OK
password.parse("debole"); // Errore
Refine con Validazione Asincrona
const emailUnica = z.string().email().refine(
async (email) => {
const esiste = await db.utente.findUnique({ where: { email } });
return !esiste;
},
{ message: "Questa email e' gia' registrata" }
);
// ATTENZIONE: devi usare parseAsync
await emailUnica.parseAsync("test@esempio.it");
Refine su Oggetti
Puoi usare .refine() per validare relazioni tra campi:
const FormPassword = z.object({
password: z.string().min(8),
confermaPassword: z.string(),
}).refine(
(dati) => dati.password === dati.confermaPassword,
{
message: "Le password non corrispondono",
path: ["confermaPassword"], // indica quale campo ha l'errore
}
);
FormPassword.parse({
password: "MiaPassword1",
confermaPassword: "MiaPassword2",
}); // Errore su "confermaPassword"
.superRefine()
Offre controllo totale sugli errori. Puoi aggiungere piu’ errori e personalizzare ogni dettaglio.
const schema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Minimo 8 caratteri",
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Deve contenere almeno una lettera maiuscola",
});
}
if (!/[0-9]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Deve contenere almeno un numero",
});
}
});
// Puo' restituire PIU' errori contemporaneamente
const result = schema.safeParse("abc");
// 3 errori: lunghezza, maiuscola, numero
superRefine con Abort Early
Puoi interrompere la validazione con NEVER:
const schema = z.object({
tipo: z.string(),
valore: z.unknown(),
}).superRefine((dati, ctx) => {
if (dati.tipo === "numero" && typeof dati.valore !== "number") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Il valore deve essere un numero",
path: ["valore"],
fatal: true, // ferma la catena di validazione
});
return z.NEVER;
}
});
.pipe()
Collega lo schema di output di uno con l’input di un altro. Utile per trasformazioni con validazione post-trasformazione.
// Parsa una stringa, trasformala in numero, poi valida il numero
const schema = z.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int().positive());
schema.parse("42"); // => 42
schema.parse("-5"); // Errore: non positivo
schema.parse("abc"); // Errore: NaN non e' un intero positivo
pipe() per Validazione a Stadi
const JsonString = z.string()
.transform((val) => JSON.parse(val))
.pipe(z.object({
nome: z.string(),
eta: z.number(),
}));
JsonString.parse('{"nome":"Marco","eta":28}');
// => { nome: "Marco", eta: 28 }
.default() e .catch()
.default()
Fornisce un valore di default quando il dato e’ undefined:
const conDefault = z.string().default("sconosciuto");
conDefault.parse(undefined); // => "sconosciuto"
conDefault.parse("Marco"); // => "Marco"
Funziona anche con funzioni:
const conTimestamp = z.date().default(() => new Date());
conTimestamp.parse(undefined); // => data corrente
.catch()
Fornisce un valore di fallback quando la validazione fallisce:
const schema = z.number().catch(0);
schema.parse(42); // => 42
schema.parse("abc"); // => 0 (invece di errore)
La differenza chiave: .default() interviene per undefined, .catch() interviene per qualsiasi errore di validazione.
const sicuro = z.string().email().catch("email@default.it");
sicuro.parse("valida@mail.it"); // => "valida@mail.it"
sicuro.parse("non-valida"); // => "email@default.it"
sicuro.parse(123); // => "email@default.it"
Esempio Completo
const ImportaDatiUtente = z.object({
nome: z.string().trim().transform(val =>
val.split(" ").map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(" ")
),
email: z.string().email().toLowerCase(),
eta: z.string()
.transform(val => parseInt(val, 10))
.pipe(z.number().int().min(0).max(150)),
ruolo: z.string().default("utente"),
newsletter: z.string()
.transform(val => val === "true" || val === "1" || val === "si")
.catch(false),
});
// Simula dati da un CSV
ImportaDatiUtente.parse({
nome: " marco ROSSI ",
email: "MARCO@Email.It",
eta: "28",
newsletter: "si",
});
// => { nome: "Marco Rossi", email: "marco@email.it", eta: 28, ruolo: "utente", newsletter: true }