Zod: validação de dados com inferência de tipos

1. Introdução ao Zod e seus fundamentos

Um dos maiores desafios no desenvolvimento com TypeScript é a desconexão entre tipos em tempo de compilação e dados em tempo de execução. Enquanto o TypeScript nos protege durante o desenvolvimento, em runtime os dados vindos de APIs, formulários ou arquivos podem ser qualquer coisa. É aí que entra o Zod.

Zod é uma biblioteca de validação de esquemas que declara schemas e automaticamente infere os tipos TypeScript correspondentes. Diferente de outras soluções, Zod coloca a inferência de tipos como característica central, eliminando a duplicação entre validação e tipagem.

Para começar, instale o Zod:

npm install zod

O exemplo mais básico demonstra o poder da biblioteca:

import { z } from 'zod';

const UserSchema = z.object({
  nome: z.string(),
  idade: z.number(),
  email: z.string().email()
});

// Tentativa de parse com dados inválidos
try {
  UserSchema.parse({ nome: "João", idade: "25", email: "invalido" });
} catch (error) {
  console.error(error.errors);
  // [
  //   { code: 'invalid_type', expected: 'number', received: 'string', path: ['idade'] },
  //   { code: 'invalid_string', validation: 'email', path: ['email'] }
  // ]
}

2. Esquemas básicos e composição

Zod oferece validação para todos os tipos primitivos e estruturas complexas:

// Tipos primitivos com refinamentos
const nomeSchema = z.string().min(3).max(100);
const idadeSchema = z.number().int().positive().max(150);
const ativoSchema = z.boolean();
const dataSchema = z.date().min(new Date("2020-01-01"));

// Estruturas compostas
const tagsSchema = z.array(z.string().max(20));
const coordenadaSchema = z.tuple([z.number(), z.number()]);

// União e interseção
const resultadoSchema = z.union([z.string(), z.number()]);
const completoSchema = z.intersection(
  z.object({ nome: z.string() }),
  z.object({ idade: z.number() })
);

// Objetos aninhados e reutilização
const EnderecoSchema = z.object({
  rua: z.string(),
  cidade: z.string(),
  uf: z.string().length(2)
});

const UsuarioCompletoSchema = z.object({
  nome: z.string(),
  endereco: EnderecoSchema,
  contatos: z.array(z.object({
    tipo: z.enum(["email", "telefone"]),
    valor: z.string()
  }))
});

3. Inferência de tipos com z.infer

A grande inovação do Zod é a capacidade de extrair tipos TypeScript diretamente dos schemas:

const UsuarioSchema = z.object({
  id: z.string().uuid(),
  nome: z.string().min(3),
  email: z.string().email(),
  idade: z.number().int().optional(),
  criadoEm: z.date()
});

// Inferência automática do tipo
type Usuario = z.infer<typeof UsuarioSchema>;

// Equivalente manual (que não precisamos mais escrever!)
// type Usuario = {
//   id: string;
//   nome: string;
//   email: string;
//   idade?: number;
//   criadoEm: Date;
// };

// Uso prático: um schema como fonte única da verdade
function processarUsuario(dados: unknown): Usuario {
  return UsuarioSchema.parse(dados);
}

Isso elimina completamente a duplicação. Qualquer alteração no schema reflete automaticamente no tipo.

4. Validação segura: parse, safeParse e tratamento de erros

Zod oferece duas abordagens para validação:

// parse() - lança exceção
try {
  const usuario = UsuarioSchema.parse(dadosBrutos);
  // continua com dados validados
} catch (error) {
  if (error instanceof z.ZodError) {
    error.errors.forEach(err => {
      console.log(`Campo: ${err.path.join('.')}`);
      console.log(`Erro: ${err.message}`);
      console.log(`Código: ${err.code}`);
    });
  }
}

// safeParse() - retorna objeto com status
const resultado = UsuarioSchema.safeParse(dadosBrutos);

if (resultado.success) {
  const usuario: Usuario = resultado.data;
  // dados validados e tipados
} else {
  const erros = resultado.error.flatten();
  console.log(erros.fieldErrors);
  // { nome: ['String must contain at least 3 character(s)'], email: ['Invalid email'] }
}

safeParse é ideal para APIs e formulários onde queremos tratar erros sem exceções.

5. Transformações e refinamentos customizados

O Zod permite modificar dados durante a validação e criar regras complexas:

const UsuarioSchema = z.object({
  nome: z.string().transform(val => val.trim()),
  email: z.string().email().transform(val => val.toLowerCase()),
  senha: z.string().min(8),
  confirmacaoSenha: z.string()
}).refine(data => data.senha === data.confirmacaoSenha, {
  message: "Senhas não conferem",
  path: ["confirmacaoSenha"]
});

// Transformações com superRefine para validações complexas
const PedidoSchema = z.object({
  itens: z.array(z.object({
    produto: z.string(),
    quantidade: z.number().positive(),
    precoUnitario: z.number().positive()
  })),
  desconto: z.number().min(0).max(100).optional()
}).superRefine((data, ctx) => {
  const total = data.itens.reduce(
    (sum, item) => sum + item.quantidade * item.precoUnitario, 
    0
  );

  if (data.desconto && data.desconto > total * 0.5) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Desconto não pode exceder 50% do total",
      path: ["desconto"]
    });
  }
});

6. Integração com APIs e serviços externos

Validar dados de APIs externas é um caso de uso crítico:

// Definindo schema para resposta de API
const ApiResponseSchema = z.object({
  status: z.enum(["success", "error"]),
  data: z.unknown(),
  pagination: z.object({
    page: z.number().int().positive(),
    totalPages: z.number().int().positive(),
    totalItems: z.number().int().nonnegative()
  }).optional()
});

// Usando com fetch
async function fetchUsuario(id: string): Promise<Usuario> {
  const response = await fetch(`https://api.exemplo.com/usuarios/${id}`);
  const dadosBrutos: unknown = await response.json();

  const validado = ApiResponseSchema.parse(dadosBrutos);

  if (validado.status === "error") {
    throw new Error("API retornou erro");
  }

  // Validando dados específicos do usuário
  return UsuarioSchema.parse(validado.data);
}

// Em um endpoint Express
import express from 'express';

const app = express();
app.post('/usuarios', (req, res) => {
  const resultado = UsuarioSchema.safeParse(req.body);

  if (!resultado.success) {
    return res.status(400).json({
      error: "Dados inválidos",
      detalhes: resultado.error.flatten().fieldErrors
    });
  }

  const usuario = resultado.data; // Tipado como Usuario
  // Processar...
  res.status(201).json(usuario);
});

7. Zod no ecossistema TypeScript moderno

O Zod se integra perfeitamente com frameworks modernos:

// tRPC - validação automática de inputs e outputs
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const appRouter = t.router({
  criarUsuario: t.procedure
    .input(UsuarioSchema)
    .mutation(async ({ input }) => {
      // input é tipado automaticamente como Usuario
      return await db.usuario.criar(input);
    })
});

// React Hook Form com resolver Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

function CadastroForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(UsuarioSchema)
  });

  return (
    <form onSubmit={handleSubmit(data => {
      // data é tipado como Usuario
      console.log(data);
    })}>
      <input {...register('nome')} />
      {errors.nome && <span>{errors.nome.message}</span>}
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <button type="submit">Cadastrar</button>
    </form>
  );
}

8. Considerações finais e boas práticas

Zod se destaca quando comparado a outras bibliotecas:

  • Yup: Mais verboso, menos integração com TypeScript
  • Joi: Excelente para Node.js, mas sem inferência nativa
  • io-ts: Mais puro funcionalmente, mas curva de aprendizado maior

Para performance, valide dados em lote quando possível:

// Em vez de validar individualmente
const usuarios = dados.map(u => UsuarioSchema.parse(u));

// Valide em lote com array
const usuariosValidados = z.array(UsuarioSchema).parse(dados);

Boas práticas finais:
- Mantenha schemas em arquivos separados por domínio
- Version seus schemas junto com as migrações de banco
- Teste schemas complexos com dados de borda
- Use z.infer para gerar tipos, nunca o contrário

Zod não é apenas uma biblioteca de validação — é a ponte que conecta a segurança do sistema de tipos do TypeScript com a realidade imprevisível dos dados em tempo de execução.

Referências