Zod Transforms
Formate, converta e adicione lógica de validação personalizada com transform, refine, superRefine, preprocess e pipe.
Busque em todas as páginas da documentação
Formate, converta e adicione lógica de validação personalizada com transform, refine, superRefine, preprocess e pipe.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
import { z } from "zod";
// transform - altera o valor de saída
const Trimmed = z.string().transform((s) => s.trim());
const Lower = z.string().transform((s) => s.toLowerCase());
// refine - predicado de validação personalizado
const EvenNumber = z.number().refine((n) => n % 2 === 0, {
message: "Deve ser um número par",
});
// superRefine - múltiplos problemas, controle total
const PasswordSchema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Pelo menos 8 caracteres" });
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Pelo menos uma letra maiúscula" });
}
if (!/\d/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Pelo menos um dígito" });
}
});
// preprocess - converte antes da validação
const CoercedNumber = z.preprocess((val) => Number(val), z.number().positive());
// pipe - encadeia esquemas juntos
const StringToNumber = z.string().pipe(z.coerce.number().int().positive());Quando usar isso: Quando a validação de tipo básica não é suficiente - você precisa normalizar dados, aplicar regras de negócios ou encadear transformações.
"use client";
import { useState } from "react";
import { z } from "zod";
const SignupSchema = z
.object({
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/, "Apenas letras, números e underscores")
.transform((s) => s.toLowerCase()),
email: z.string().email().transform((s) => s.toLowerCase().trim()),
password: z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Pelo menos 8 caracteres" });
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de uma letra maiúscula" });
}
if (!/[0-9]/.test(val)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de um dígito" });
}
}),
confirmPassword: z.string(),
age: z.preprocess((val) => Number(val), z.number().int().min(13, "Deve ter 13+")),
})
.refine((data) => data.password === data.confirmPassword, {
message: "As senhas não coincidem",
path: ["confirmPassword"],
});
type SignupInput = z.input<typeof SignupSchema>;
type SignupOutput = z.output<typeof SignupSchema>;
export function SignupForm() {
const [output, setOutput] = useState<string>("");
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const raw = Object.fromEntries(fd);
const result = SignupSchema.safeParse(raw);
if (result.success) {
setOutput("Válido: " + JSON.stringify(result.data, null, 2));
} else {
setOutput(result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("\n"));
}
}
return (
<form onSubmit={handleSubmit} className="flex max-w-md flex-col gap-3">
<input name="username" placeholder="Nome de usuário" className="rounded border p-2" />
<input name="email" placeholder="Email" className="rounded border p-2" />
<input name="password" type="password" placeholder="Senha" className="rounded border p-2" />
<input name="confirmPassword" type="password" placeholder="Confirmar" className="rounded border p-2" />
<input name="age" placeholder="Idade" type="number" className="rounded border p-2" />
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">Cadastrar</button>
{output && <pre className="rounded bg-gray-50 p-3 text-sm whitespace-pre-wrap">{output}</pre>}
</form>
);
}O que isso demonstra:
transform para normalizar nome de usuário e emailsuperRefine para validação de senha com múltiplas regraspreprocess para converter a idade de string para número.refine em nível de objeto para validação entre campos (correspondência de senha)false cria um único problemaRefinementCtx para que você possa adicionar múltiplos problemasz.input e z.output podem diferirRefine assíncrono para verificações do lado do servidor:
const UniqueEmail = z.string().email().refine(
async (email) => {
const exists = await db.user.findUnique({ where: { email } });
return !exists;
},
{ message: "Email já registrado" }
);
// Deve usar parseAsync / safeParseAsync
const result = await UniqueEmail.safeParseAsync("test@example.com");Encadeando transforms:
const CsvToNumbers = z
.string()
.transform((s) => s.split(","))
.transform((arr) => arr.map(Number))
.pipe(z.array(z.number().finite()));Tipos "branded" com refine:
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>; // string & { __brand: "UserId" }
function getUser(id: UserId) { /* ... */ }
// getUser("random-string") - erro de compilação
// getUser(UserId.parse("550e...")) - funcionaPipeline de padrão + transform:
const Config = z.object({
port: z.coerce.number().default(3000),
host: z.string().default("localhost").transform((h) => h.toLowerCase()),
debug: z.preprocess((v) => v === "true" || v === true, z.boolean()).default(false),
});// Quando um esquema tem transforms, os tipos de entrada e saída diferem
const S = z.string().transform((s) => s.length);
type SInput = z.input<typeof S>; // string
type SOutput = z.output<typeof S>; // number
type SInfer = z.infer<typeof S>; // number (igual a output)
// superRefine pode estreitar tipos
const NonEmpty = z.array(z.string()).superRefine((arr, ctx): arr is [string, ...string[]] => {
if (arr.length === 0) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de pelo menos um" });
return false;
}
return true;
});refine é executado apenas se a validação base for bem-sucedida - Se z.string().email().refine(...) receber um valor que não é um email, o callback de refine nunca é chamado. Isso geralmente é desejável, mas pode surpreendê-lo se você espera todos os erros de uma vez.
transform muda o tipo - Após .transform(), os validadores subsequentes veem o tipo transformado. Se você encadear .min() após .transform(), ele valida o valor transformado. Correção: Coloque os validadores antes dos transforms.
preprocess vs coerce - z.preprocess é um hook de propósito geral; z.coerce.* é um atalho para z.preprocess(Constructor, ...). Prefira z.coerce para casting de tipo simples.
Refinamentos assíncronos exigem parseAsync - Chamar .parse() em um esquema com refinamentos assíncronos lança uma exceção. Correção: Sempre use parseAsync ou safeParseAsync.
Caminho de refine em nível de objeto - Se você omitir path em um .refine() de objeto, o erro aparece em formErrors em vez de fieldErrors. Correção: Sempre especifique path: ["fieldName"] para refinamentos em nível de objeto.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
Yup .test() | Você usa Formik e precisa de validadores personalizados | Você quer tipos transformados inferidos pelo TypeScript |
Valibot transform | Você precisa de transforms "tree-shakeable" com bundle mínimo | Você depende de .brand() ou .pipe() específicos do Zod |
| Funções de validação personalizadas | A lógica é trivial (um campo, uma verificação) | Você tem dependências complexas entre vários campos |
Decorators class-validator | Você prefere validação baseada em decorators em modelos de classe | Você trabalha em uma base de código funcional / baseada em componentes |
transform muda o valor de saída (e possivelmente seu tipo)refine adiciona um único predicado de validação personalizado (retorna true/false)superRefine fornece acesso total ao RefinementCtx para que você possa adicionar múltiplos problemasz.coerce.* é um atalho para casting de tipo simples (por exemplo, string para número via Number()). Use z.preprocess quando precisar de lógica de conversão personalizada além do que um construtor nativo fornece.
pipe passa a saída de um esquema como entrada para outro. É útil para transformações em várias etapas:
const CsvToNumbers = z.string()
.transform((s) => s.split(","))
.transform((arr) => arr.map(Number))
.pipe(z.array(z.number().finite()));const Email = z.string().email().transform((s) => s.toLowerCase().trim());
const Username = z.string().min(3).transform((s) => s.toLowerCase());Coloque os validadores antes dos transforms para que eles verifiquem a entrada bruta.
const Password = z.string().superRefine((val, ctx) => {
if (val.length < 8)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Pelo menos 8 caracteres" });
if (!/[A-Z]/.test(val))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de maiúscula" });
if (!/\d/.test(val))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de dígito" });
});Use .refine() no esquema do objeto com a opção path:
schema.refine((d) => d.password === d.confirmPassword, {
message: "As senhas não coincidem",
path: ["confirmPassword"],
});const UniqueEmail = z.string().email().refine(
async (email) => !(await db.user.findUnique({ where: { email } })),
{ message: "Email já registrado" }
);
// Deve usar parseAsync ou safeParseAsyncNão. Se z.string().email().refine(...) receber um valor que não é um email, o callback de refine nunca é chamado. A validação base deve passar primeiro. Isso significa que você não verá todos os erros de uma vez se o tipo base estiver incorreto.
Ele lança um erro. Você deve usar parseAsync() ou safeParseAsync() para qualquer esquema que contenha refinamentos assíncronos.
Após .transform(), os validadores subsequentes veem o valor transformado. Se você encadear .min() após .transform(), ele valida a saída transformada, não a entrada original.
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>;
// string & { __brand: "UserId" }
// Impede a passagem de strings arbitrárias onde um UserId é esperadoconst S = z.string().transform((s) => s.length);
type SInput = z.input<typeof S>; // string
type SOutput = z.output<typeof S>; // numberOs tipos de entrada e saída divergem sempre que um transform muda o tipo.
Sim. Use um retorno de predicado de tipo:
const NonEmpty = z.array(z.string()).superRefine(
(arr, ctx): arr is [string, ...string[]] => {
if (arr.length === 0) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Precisa de um" });
return false;
}
return true;
}
);Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥