Banner da postagem: Zod 4: Guia Prático para Validação de Dados
JavaScript
Tutorial
TypeScript

Zod 4: Guia Prático para Validação de Dados

Aprenda a validar dados com Zod 4 em TypeScript. Guia prático com coerce, enums, safeParse, React Hook Form e as novidades mais rápidas da biblioteca.


O Zod é uma biblioteca de validação de esquemas que gera tipos TypeScript automaticamente. Com o Zod 4, ela ficou 14x mais rápida e muito mais poderosa. Vou te mostrar tudo que você precisa saber para usar no dia a dia.


Sumário

  1. Instalação
  2. Conceitos Básicos
  3. Coerce - Conversão Automática
  4. Validações de String
  5. Validações de Número
  6. Enum - Valores Específicos
  7. Arrays
  8. Optional vs Nullable vs Nullish
  9. Formulários com React Hook Form
  10. Validações Customizadas
  11. Transformações
  12. Valores Padrão
  13. Parse vs SafeParse
  14. Reutilização de Schemas
  15. Novidades do Zod 4
  16. Métodos Úteis para Objetos
  17. Dicas Importantes
  18. Conclusão

Instalação

npm install zod@^4.0.0

Conceitos Básicos

Tipos Primitivos

import { z } from 'zod';

// Básicos
z.string();
z.number();
z.boolean();
z.date();

// Usando
const name = z.string();
name.parse("João"); // ✅ "João"
name.parse(123); // ❌ Erro!

Objetos

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(18)
});

// Validando
const user = userSchema.parse({
  name: "Maria",
  email: "maria@email.com",
  age: 25
}); // ✅ Funciona!

// Gerando tipo automaticamente
type User = z.infer<typeof userSchema>;
// { name: string; email: string; age: number }

Coerce - Conversão Automática

Problema: Formulários HTML sempre retornam strings, mas você precisa de números.

// ❌ Quebra - input HTML retorna "25" (string)
const badSchema = z.object({
  age: z.number()
});

// ✅ Funciona - converte "25" para 25 automaticamente
const goodSchema = z.object({
  age: z.coerce.number()
});

// Outros exemplos úteis
z.coerce.number()    // "123" → 123
z.coerce.boolean()   // "true" → true
z.coerce.date()      // "2024-01-01" → new Date()

Validações de String

// Validações básicas
z.string().min(3, "Muito curto")
z.string().max(100, "Muito longo")
z.string().email("Email inválido")
z.string().url("URL inválida")

// Formatação automática
z.string().trim()        // Remove espaços
z.string().toLowerCase() // Converte para minúsculo
z.string().toUpperCase() // Converte para maiúsculo

// Exemplo completo
const passwordSchema = z.string()
  .min(8, "Mínimo 8 caracteres")
  .regex(/[A-Z]/, "Deve ter maiúscula")
  .regex(/[0-9]/, "Deve ter número");

Validações de Número

z.number().positive()      // > 0
z.number().min(0)         // >= 0
z.number().max(100)       // <= 100
z.number().int()          // Inteiro
z.number().multipleOf(5)  // Múltiplo de 5

// Exemplo prático
const priceSchema = z.coerce.number()
  .positive("Preço deve ser positivo")
  .max(10000, "Preço muito alto");

Enum - Valores Específicos

// Opções limitadas
const statusSchema = z.enum(["pending", "approved", "rejected"]);

// Em objetos
const orderSchema = z.object({
  id: z.string(),
  status: z.enum(["pending", "shipped", "delivered"]),
  priority: z.enum(["low", "medium", "high"])
});

Arrays

// Array de strings
z.array(z.string())

// Array de objetos
z.array(z.object({
  name: z.string(),
  age: z.number()
}))

// Com validações
z.array(z.string())
  .min(1, "Deve ter pelo menos 1 item")
  .max(10, "Máximo 10 itens")

Optional vs Nullable vs Nullish

A diferença que confunde muita gente!

const schema = z.object({
  // OPCIONAL: pode não existir
  age: z.number().optional(),
  
  // NULLABLE: pode ser null (mas deve existir)
  avatar: z.string().nullable(),
  
  // NULLISH: pode ser undefined OU null
  bio: z.string().nullish()
});

// Exemplos válidos:
schema.parse({
  avatar: null,      // ✅ nullable aceita null
  bio: undefined     // ✅ nullish aceita undefined
  // age não precisa existir
});

Resumo rápido:

  • .optional() = pode ser undefined ou não existir
  • .nullable() = pode ser null (mas deve existir)
  • .nullish() = pode ser undefined ou null

Formulários com React Hook Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const formSchema = z.object({
  name: z.string().min(1, "Nome obrigatório"),
  email: z.string().email("Email inválido"),
  age: z.coerce.number().min(18, "Maior de idade"),
  type: z.enum(["user", "admin"])
});

type FormData = z.infer<typeof formSchema>;

function MyForm() {
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema)
  });

  const onSubmit = (data: FormData) => {
    console.log(data); // Dados já validados!
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* seus campos aqui */}
    </form>
  );
}

Validações Customizadas

// Validação simples
const passwordSchema = z.string()
  .refine(
    (password) => password.includes("@") || password.includes("#"),
    { message: "Deve conter @ ou #" }
  );

// Validação entre campos
const signupSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string()
})
.refine(
  (data) => data.password === data.confirmPassword,
  { message: "Senhas não conferem", path: ["confirmPassword"] }
);

Transformações

// Limpar e formatar dados
const userSchema = z.object({
  name: z.string().trim().toLowerCase(),
  email: z.string().email().toLowerCase(),
  age: z.coerce.number()
});

// Input: { name: "  JOÃO  ", email: "JOAO@EMAIL.COM", age: "25" }
// Output: { name: "joão", email: "joao@email.com", age: 25 }

Valores Padrão

// Valor padrão simples
z.string().default("não informado")

// Valor padrão com função
z.date().default(() => new Date())

// Exemplo prático
const configSchema = z.object({
  theme: z.enum(["light", "dark"]).default("light"),
  notifications: z.boolean().default(true)
});

Parse vs SafeParse

// parse() - joga erro se inválido
try {
  const result = schema.parse(data);
  console.log(result);
} catch (error) {
  console.log(error.errors);
}

// safeParse() - retorna success/error
const result = schema.safeParse(data);
if (result.success) {
  console.log(result.data);
} else {
  console.log(result.error.errors);
}

Reutilização de Schemas

// Schema base
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string()
});

// Reutilizando
const userSchema = z.object({
  name: z.string(),
  homeAddress: addressSchema,
  workAddress: addressSchema.optional()
});

Novidades do Zod 4

Formatos de String no Nível Superior

// Agora você pode usar direto (mais limpo)
z.email()
z.url()
z.uuid()
z.ipv4()
z.ipv6()

// Ao invés de (ainda funciona, mas deprecated)
z.string().email()
z.string().url()

Template Literals

// Validar padrões específicos
const cssUnit = z.templateLiteral([z.number(), z.enum(["px", "em", "rem"])]);
// Aceita: "10px", "1.5em", "2rem"

const greeting = z.templateLiteral(["hello, ", z.string()]);
// Aceita: "hello, world", "hello, João"

Stringbool - Para Variáveis de Ambiente

const envSchema = z.object({
  DEBUG: z.stringbool(), // converte "true"/"false" para boolean
  PORT: z.coerce.number()
});

// Aceita: "true", "1", "yes", "on" → true
// Aceita: "false", "0", "no", "off" → false

Métodos Úteis para Objetos

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  password: z.string()
});

// Pegar apenas alguns campos
const publicUser = userSchema.pick({ 
  id: true, 
  name: true, 
  email: true 
});

// Excluir campos
const userWithoutPassword = userSchema.omit({ 
  password: true 
});

// Todos os campos opcionais
const partialUser = userSchema.partial();

Dicas Importantes

  1. Use z.coerce para formulários HTML
  2. Prefira z.email() ao invés de z.string().email()
  3. Use safeParse quando não tiver certeza se os dados são válidos
  4. Reutilize schemas para manter consistência
  5. Combine com React Hook Form para formulários perfeitos

Conclusão

O Zod 4 é uma evolução gigantesca que torna a validação de dados muito mais simples e performática. Com 14x mais velocidade e APIs mais limpas, é a escolha perfeita para qualquer projeto TypeScript.

Principais benefícios:

  • Type Safety automático
  • Coerce para formulários HTML
  • Performance muito superior
  • API limpa e intuitiva
  • Integração perfeita com React Hook Form

Comece com o básico e vá explorando os recursos mais avançados conforme a necessidade!