Skip to content
This page has been auto-translated and may contain errors.View in English

Fundamentos do Zod

Três bugs até agora, todos com o mesmo formato. Um valor chegou, o código o usou, e ninguém o conferiu antes.

Corrigir cada um deles no lugar onde aparece funciona, mas é uma tarefa sem fim. Todo destino novo é mais um lugar para lembrar de checar.

A outra abordagem é conferir o valor uma única vez, assim que ele chega, contra uma descrição do que você esperava receber. Essa descrição é um schema, e o Zod é a biblioteca que esta seção usa para escrever um.

O que é um schema

Imagine o scanner de um aeroporto. Sua mala entra por um lado. A máquina foi configurada com regras: nada de cortantes, nada de líquidos acima de certo tamanho. Uma mala que satisfaz as regras sai do outro lado sem alterações. Uma mala que não satisfaz nunca passa.

Um schema é a configuração da máquina. Fazer o parse é passar a mala pelo scanner.

Instale e importe:

bash
npm install zod
js
import * as z from 'zod'

O schema mais simples é uma única regra sobre um único valor:

js
const teacherSchema = z.string()

const teacher = 'Jonathan'

console.log(teacherSchema.parse(teacher))
// 'Jonathan'

parse entrega o dado para a máquina. O dado satisfaz a regra, então ele volta intacto.

Passe algo que não se encaixa e a máquina para:

js
teacherSchema.parse(12345)
// ZodError: Invalid input: expected string, received number

Esse é o modelo inteiro. Descreva o que é aceitável, passe valores por ali, receba o valor de volta ou receba um erro.

JunoO que é um schema Duas palavras que vale a pena separar desde já, porque costumam ser usadas como sinônimos e aqui significam coisas diferentes.

O schema é a descrição do que você vai aceitar. O parse é o ato de conferir algo contra ele. Você escreve o schema uma vez e faz o parse com ele quantas vezes quiser.

JunoO que é um schema Repare que parse devolve o valor, não um verdadeiro ou falso. Isso é proposital: é um ponto de checagem por onde os dados passam, não um teste que você roda ao lado deles.

O hábito que vem em seguida é parar de usar o input bruto depois de um parse e usar o valor retornado em vez dele. É o mesmo dado hoje, e isso garante que o valor "parseado" é o que segue adiante quando você começar a adicionar valores padrão e coerção.

JunoO que é um schema A API é imutável, algo discreto o suficiente para passar despercebido e gerar um bug de verdade. Cada método retorna um novo schema em vez de modificar aquele em que foi chamado, então schema.min(3) é um valor que você precisa guardar.

Chame o método e descarte o resultado, e o schema original fica inalterado, com a restrição silenciosamente ausente.

parse lança uma exceção, o que serve bem para um limite onde uma falha deve interromper a requisição, e não serve para nada onde você queira inspecionar a falha. A alternativa que retorna um objeto de resultado em vez de lançar exceção é a que você vai querer em um formulário ou em um handler de rota, e ela é abordada no próximo capítulo.

Descrevendo um objeto

Um professor raramente é apenas uma string. Em vez disso, dê ao schema um formato:

js
const teacherSchema = z.object({
  name: z.string(),
  age: z.number(),
})

const teacher = {
  name: 'Jonathan',
  age: 21,
}

console.log(teacherSchema.parse(teacher))
// { name: 'Jonathan', age: 21 }

Cada entrada dentro de z.object() é uma chave, e seu valor é o schema daquela chave. Agora a máquina espera um objeto com um name que seja qualquer string e um age que seja qualquer número.

Quebre um deles e o erro diz exatamente qual:

js
teacherSchema.parse({ name: 'Jonathan', age: '21' })
// ZodError: Invalid input: expected number, received string

O Zod já vem com os tipos primitivos que você esperaria, z.string(), z.number(), z.boolean(), e objetos se aninham dentro de objetos tão profundamente quanto seus dados exigirem.

JunoDescrevendo um objeto O aninhamento é a parte que faz isso escalar. Um schema para um objeto é construído a partir dos schemas de seus campos, e cada um desses campos pode, ele mesmo, ser um objeto.

Assim, a mesma ideia cobre tanto um formulário de dois campos quanto uma resposta de API profundamente estruturada. Você está sempre descrevendo um nível de cada vez.

JunoDescrevendo um objeto Vale saber antes que isso te pegue de surpresa: z.object() ignora chaves que você não descreveu. Faça o parse de { name: 'A', age: 1, extra: 'x' } contra um schema que descreve apenas name e age, e você recebe de volta { name: 'A', age: 1 }, sem erro e sem o extra.

Isso costuma ser o que você quer numa fronteira de entrada, já que significa que um atacante não consegue contrabandear um campo extra para dentro do que quer que você faça com o objeto parseado. Também significa que um erro de digitação no nome de um campo falha silenciosamente, então um valor que você esperava que estivesse ali fica ausente sem aviso.

JunoDescrevendo um objeto Quando você quiser o comportamento mais rígido, z.strictObject() gera um erro para chaves não reconhecidas em vez de descartá-las. Verificado no Zod 4.5.4: z.object() remove, z.strictObject() lança exceção.

Qual usar é uma decisão real, não uma questão de estilo. Remover é o padrão mais seguro para um endpoint público, porque clientes adicionam campos e você não quer quebrar o funcionamento deles.

O modo estrito serve bem para chamadas internas entre serviços, onde uma chave inesperada geralmente significa que os dois lados se desalinharam. É melhor descobrir isso na hora do que depurar um campo silenciosamente descartado uma semana depois.

O único ponto em que a remoção automática morde é o raciocínio de "mass assignment". Ela protege o objeto parseado, mas não faz nada por um código que ultrapassa o parse e lê req.body diretamente. Faça o parse uma vez e nunca mais olhe para o input bruto.

Por que a checagem precisa acontecer em tempo de execução

O TypeScript também descreve formatos:

ts
type Teacher = {
  name: string
  age: number
}

Isso parece com o schema e cumpre uma função completamente diferente. O TypeScript verifica tipos enquanto você escreve o código. Quando ele compila para JavaScript, toda anotação é apagada, então nada sobrevive no programa em execução.

O que é ótimo para valores que seu próprio código produziu, e inútil para valores que ele não produziu. Um corpo de requisição, uma resposta de API, um envio de formulário: essas coisas chegam enquanto o programa está rodando, muito depois de os tipos terem deixado de existir. O TypeScript assume que seus dados estão corretos. O Zod confere.

Um schema é a descrição de um tipo que ainda existe no momento em que o dado aparece.

JunoPor que a checagem precisa acontecer em tempo de execução Isso confunde as pessoas porque as duas coisas parecem estar checando a mesma coisa, e só uma delas está presente quando realmente importa.

O TypeScript é uma conversa com você enquanto você escreve. O schema é uma conversa com o dado enquanto o programa roda.

JunoPor que a checagem precisa acontecer em tempo de execução Tipar o corpo de uma requisição é onde isso aparece com mais frequência. Convertê-lo para um tipo que você escreveu, com req.body as SignupFields, compila sem problemas e não confere nada, porque uma asserção de tipo é você dizendo ao compilador para parar de perguntar.

Cada campo que você lê depois disso é uma suposição. Faça o parse do corpo com um schema em vez disso, e a suposição vira um fato.

JunoPor que a checagem precisa acontecer em tempo de execução A fronteira que vale a pena traçar é: tudo que atravessou um limite de processo é não tipado, não importa o que suas anotações afirmem. Corpos de requisição, query strings, variáveis de ambiente, JSON lido do disco, respostas de um serviço que seu próprio time mantém, linhas de um banco de dados cuja migration você ainda não rodou.

Respostas de APIs de terceiros são o caso que as pessoas costumam deixar de fora, com o argumento de que o provedor documenta o formato. Provedores lançam mudanças, retornam objetos parciais durante incidentes e adicionam nulos a campos que nunca tinham sido nulos antes. Um schema nessa fronteira transforma uma falha confusa lá no fundo do seu código em uma falha clara no ponto de entrada.

O Zod vale seu tamanho por causa disso: sem dependências, e roda do mesmo jeito no Node e no navegador, então um único schema pode servir tanto o handler de uma rota quanto o formulário que envia dados para ela.

Mão na massa

Escreva do zero, sem copiar o exemplo do professor.

  1. Importe o Zod.
  2. Crie um characterSchema com duas chaves: name, uma string, e episode, um número.
  3. Defina um objeto character com o nome 'Luke Skywalker' e episódio 4.
  4. Valide o character contra o schema e exiba o resultado no console.
  5. Mude episode para a string '4' e preveja o que vai acontecer antes de rodar o código.
Compare suas respostas
js
import * as z from 'zod'

const characterSchema = z.object({
  name: z.string(),
  episode: z.number(),
})

const character = {
  name: 'Luke Skywalker',
  episode: 4,
}

console.log(characterSchema.parse(character))
// { name: 'Luke Skywalker', episode: 4 }

Duas chaves significam z.object() em vez de um primitivo isolado, e cada chave recebe seu próprio schema.

Com episode como '4', parse lança ZodError: Invalid input: expected number, received string. A string '4' não é um número, e o Zod não vai convertê-la silenciosamente para você. Converter de propósito é uma instrução separada, coberta no próximo capítulo.

Episódio 4 está correto, por sinal. Luke Skywalker aparece pela primeira vez no filme original de Star Wars de 1977, que mais tarde foi numerado como Episódio IV.

Para onde isso vai a seguir

Por enquanto, um schema faz uma única coisa: aceita um valor ou lança uma exceção. Isso já basta para descrever dados, mas ainda não basta para construir algo com eles.

Inferindo tipos e convertendo entradas adiciona as duas peças que tornam isso prático. Um único schema pode entregar o tipo também ao TypeScript, de modo que o formato seja escrito apenas uma vez. E um parse malsucedido pode te devolver um resultado para inspecionar, em vez de uma exceção para capturar.