00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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 }