JSON para Zod

Gera esquemas Zod a partir de JSON, cada um com seu tipo z.infer: elementos de array mesclados, chaves opcionais e nullable marcadas, nada adivinhado.

Entrada
Esquema Zod
import * as z from "zod"

export const Customer = z.object({
  name: z.string(),
  email: z.string(),
  phone: z.null(),
})
export type Customer = z.infer<typeof Customer>

export const LineItem = z.object({
  sku: z.string(),
  quantity: z.number(),
  price: z.number(),
  note: z.string().nullable(),
  backordered: z.boolean().optional(),
})
export type LineItem = z.infer<typeof LineItem>

export const Root = z.object({
  id: z.string(),
  customer: Customer,
  lineItems: z.array(LineItem),
  tags: z.array(z.unknown()),
})
export type Root = z.infer<typeof Root>

Sintaxe do Zod 4 que também é válida no Zod 3. O z.object descarta toda chave que não lista, então um campo que faltou na sua amostra some sem aviso dos dados que ele analisa.

  • Em cada posição abaixo, sua amostra só teve null, então o esquema não aceita mais nada ali.

    Posições: 1

    • Customer.phone
  • Em cada posição abaixo, todo número era inteiro. z.number() também aceita 7.5, e .int() exigiria números inteiros, então só o adicione você mesmo onde souber que um valor precisa ser inteiro.

    Posições: 1

    • LineItem.quantity
  • Em cada posição abaixo, nada pôde ser inferido, então o esquema não verifica o que há ali: z.unknown() aceita qualquer valor, e z.record(z.string(), z.unknown()), qualquer objeto.

    Posições: 1

    • Root.tags[]

Uma verificação que roda cada vez que os dados chegam

Um tipo TypeScript é verificado quando seu código compila e já não existe quando ele roda. Um esquema Zod é a parte que fica: ele roda dentro do seu programa e verifica cada payload à medida que ele chega — uma resposta de API, o corpo de um webhook, uma mensagem tirada de uma fila — antes que seu código dependa dele. Cole uma amostra desse JSON e esta página escreve os esquemas para você, prontos para colar em um módulo: todo objeto que tem chaves vira um "z.object" com nome, os elementos de um array mesclados em um só, e cada esquema é seguido do tipo TypeScript que ele produz.

É a resposta que a página JSON para TypeScript dá para o mesmo JSON, escrita como um validador em vez de como tipos. As duas páginas leem uma única inferência, então quais chaves são opcionais, quais valores viram uma união, onde null fica separado de uma chave ausente e como cada objeto aninhado se chama são decididos uma vez e apenas escritos duas vezes. Essas regras de mesclagem são o assunto do guia da página JSON para TypeScript e não são contadas de novo aqui. Este guia trata do que o esquema faz com dados reais depois que roda, e do que ele deixa para você decidir.

O que passa e o que é rejeitado

Um esquema desta página aceita a amostra a partir da qual foi escrito, no Zod 3 e no Zod 4 igualmente, exceto no único caso que tem um aviso próprio, um número infinito no Zod 4. Além dessa amostra, ele aceita tudo o que fica dentro do que o esquema diz, o que, para os dados que você vai de fato receber, funciona assim:

  • Aquém do limite de profundidade, uma chave escrita sem ".optional()" é obrigatória. Um payload que a omite é rejeitado, e o mesmo vale para um que põe ali um tipo de valor que o esquema não nomeia — uma string onde ele diz "z.number()", um objeto onde ele diz "z.string()".
  • Uma chave escrita com ".optional()" pode faltar, e uma escrita com ".nullable()" pode conter null. Nenhuma das duas deixa passar qualquer outro tipo de valor, então uma chave opcional que está presente ainda precisa conter o que o esquema nomeia.
  • Um "z.union" aceita qualquer um dos seus membros e nada mais. Um "z.array" aceita qualquer número de elementos, inclusive nenhum em absoluto, desde que cada um se encaixe no esquema escrito para os seus elementos.
  • Onde a amostra não mostrou nada — os elementos de um array vazio, um objeto sem chaves, um valor aninhado além do limite de profundidade — o esquema não verifica o que há ali: aceita qualquer valor, ou qualquer objeto onde o objeto da amostra não tinha chaves, e um aviso diz onde.

Uma chave que o esquema não lista é deixada passar e depois descartada do resultado; isso é o descarte, e ele tem uma seção própria. Uma chave fica de fora de tudo isso. Uma chave com a grafia "__proto__" é escrita como uma chave computada, entre colchetes, porque, escrita de forma simples, ela definiria o protótipo do objeto em que está em vez de nomear uma chave — e, no entanto, nenhuma das duas versões do Zod devolve essa chave no que uma análise retorna, e o Zod 4 não verifica o valor dela de forma alguma.

Por que o esquema nunca se aperta por conta própria

Toda amostra tem coisas em comum que não consegue provar: todo id um número inteiro, todo e-mail com formato de e-mail, um papel que nunca disse outra coisa senão admin. Isso é uma regularidade, e uma regularidade não é uma verificação. Um gerador fica tentado a escrevê-la mesmo assim — ".int()" nos ids, "z.email()" nos endereços, um literal no papel — e este nunca o faz, porque uma amostra mostra o que seus dados podem conter e nunca o que eles precisam conter. Um tipo mais apertado que seus dados custa um erro de compilação na sua própria máquina. Um esquema mais apertado que seus dados custa uma requisição rejeitada em produção: o primeiro papel que não é admin, o primeiro id igual a 7.5, o primeiro endereço que o padrão não previu.

As verificações também mudariam sob os seus pés. O "z.uuid()" do Zod 4 rejeita strings com formato de UUID que o ".uuid()" do Zod 3 aceitava, porque verifica os bits de variante, e o ".int()" do Zod 4 rejeita um inteiro fora do intervalo seguro que o do Zod 3 deixava passar — então uma verificação adivinhada a partir da amostra de hoje seria uma verificação diferente conforme o Zod que você instalar. Nada que a amostra apenas sugira é escrito no esquema, portanto. Onde isso importa quando os dados chegam, a página conta a você em um aviso em vez disso, e a edição fica por sua conta.

O que o esquema não diz por si mesmo

Abaixo do esquema, a página lista o que notou e não escreveu: um aviso para cada espécie abaixo que se aplica, dado uma vez, com todas as posições em que vale. Uma posição é escrita do jeito que a saída nomeia as coisas — "Customer.phone" para uma chave, "Root.tags[]" para os elementos de um array, a chave entre aspas e colchetes onde ela não é um identificador e para "__proto__" — de modo que possa ser encontrada no esquema num relance. O exemplo que carrega com a página mostra todas as espécies, menos a de Infinity.

  • Só null. A amostra nunca teve nada além de null naquela posição, então o esquema diz "z.null()" e rejeita o primeiro valor real. Decida o que o campo contém quando é preenchido e escreva isso você mesmo — "z.string().nullable()", por exemplo — ou cole uma amostra em que ele tenha um valor.
  • Infinity. Um número além do que um número do JavaScript consegue armazenar, como 1e999, vira Infinity ou -Infinity quando o JSON é analisado. O "z.number()" do Zod 4 rejeita um número infinito e o do Zod 3 o aceita, então no Zod 4 este é o único caso em que um esquema rejeita a própria amostra a partir da qual foi feito. A pergunta que isso levanta é sobre os dados e não sobre o esquema: os dígitos se perdem antes que qualquer validador os veja, então cabe ao que quer que o tenha escrito responder se esse valor deveria sequer ser um número.
  • Números inteiros. Todo número naquela posição era inteiro, e "z.number()" também aceita 7.5. Onde um valor precisa ser inteiro — um id, uma contagem, uma quantidade — adicione ".int()" você mesmo; onde é um preço que por acaso deu redondo, deixe como está. O aviso não é dado em nenhuma posição que contenha um inteiro fora do intervalo seguro, de "Number.MIN_SAFE_INTEGER" a "Number.MAX_SAFE_INTEGER", porque o ".int()" do Zod 4 os rejeita, e um conselho que faria um esquema rejeitar a própria amostra é o único tipo que a página não dá.
  • Nada a inferir. Os elementos de um array vazio, um objeto sem chaves e um valor aninhado a mais de 100 níveis de profundidade não dão à inferência nada em que se basear, então o esquema não verifica o que há ali: "z.unknown()" aceita qualquer valor, seja ele qual for, e "z.record(z.string(), z.unknown())", qualquer objeto. Cole uma amostra em que esse array tenha elementos e esse objeto tenha chaves, ou escreva essa parte do esquema à mão.

Um aviso nunca muda um byte do esquema. É uma frase ao lado dele, no idioma da página, e a edição para a qual ele aponta cabe a você fazer ou dispensar. Não há aviso sobre formatos de string, literais ou enums: cada um seria adivinhar uma regra que a amostra não consegue mostrar.

Chaves que o esquema não lista são descartadas

O "z.object" deixa passar um objeto que carrega chaves que ele não lista e as deixa de fora do que retorna. Esse é o padrão do Zod nas duas versões, e a coisa mais silenciosa que um esquema faz: um campo que por acaso faltou na sua amostra some dos dados que seu código recebe, sem nenhum erro que o diga. É por isso que a frase embaixo de cada esquema desta página fala disso.

A página não escolhe entre estrito e permissivo por você, porque cada um afirma mais do que uma amostra consegue mostrar. Um objeto estrito rejeita qualquer chave que não lista — nenhuma chave além destas — e nenhuma amostra consegue provar isso sobre o próximo payload. Um objeto permissivo mantém as chaves extras, e seu tipo inferido ganha uma assinatura de índice para elas, então ele deixaria de ser a resposta da página de TypeScript. O descarte é o único comportamento que aceita o que o tipo TypeScript aceita e ainda assim infere esse tipo — um valor com propriedades extras também satisfaz uma interface. Para escolher de outro modo, mude o esquema à mão:

  • Para rejeitar chaves que o esquema não lista, escreva "z.strictObject" onde a saída escreve "z.object" no Zod 4, ou encadeie ".strict()" no "z.object" no Zod 3.
  • Para mantê-las, escreva "z.looseObject" no Zod 4, ou encadeie ".passthrough()" no Zod 3. O Zod 4 ainda roda esses dois métodos do Zod 3, e os chama de legados.

Cada objeto da saída é um esquema próprio, então a escolha é feita um objeto de cada vez: tornar a raiz estrita não muda nada nos objetos aninhados dentro dela, o que muitas vezes é o que você quer quando é só no envelope externo que você tem o direito de insistir.

Um só nome para cada esquema e seu tipo

Cada esquema é seguido do seu tipo — "export type Customer = z.infer<typeof Customer>" logo depois de "export const Customer" —, que é como o zod.dev escreve os próprios exemplos: um só nome para o valor que verifica os dados e para o tipo que ele produz, já que o TypeScript mantém valores e tipos em espaços de nomes separados. Esse tipo é o que a página JSON para TypeScript imprime para o mesmo JSON, nome por nome e chave por chave, com as mesmas chaves opcionais, as mesmas uniões e null nos mesmos lugares — salvo as exceções abaixo.

O repositório verifica essa promessa em vez de confiar nela: um corpus de amostras passa pelas duas páginas e depois pelo compilador do TypeScript com cada versão do Zod, e o compilador é questionado, para cada nome, se os dois tipos são idênticos e se cada um é atribuível ao outro. Onde ele responde outra coisa, o lugar fica nas profundezas de um documento. Além do limite de profundidade, onde uma chave contém "z.unknown()", o Zod 3 infere essa chave como opcional, enquanto a página de TypeScript a torna obrigatória. E, no Zod 4, o compilador desiste do tipo de um array aninhado a dezenas de níveis de profundidade, relatando o erro TS2589 na própria linha do tipo; o esquema acima dessa linha continua rodando e continua aceitando a amostra, e só o tipo inferido se perde.

As duas comparações leem uma chave opcional do jeito que o TypeScript faz por padrão. Sob "exactOptionalPropertyTypes", que fica desligado a menos que um projeto o ligue, o "z.infer" de uma chave opcional também admite um undefined explícito, em qualquer das duas versões do Zod, enquanto o tipo da página de TypeScript não o admite.

Escrito para o Zod 4, e ainda válido no Zod 3

A saída se atém ao que as duas versões principais têm — "z.object", "z.array", "z.union" com dois ou mais membros, "z.string()", "z.number()", "z.boolean()", "z.null()", "z.unknown()", "z.record(z.string(), z.unknown())", ".optional()", ".nullable()" e "z.infer" —, sob a linha de importação com que os próprios exemplos do zod.dev começam. Nada nela chegou com o Zod 4 e nada nela está obsoleto lá, então um projeto que não saiu do Zod 3 pode colá-la como está.

O mesmo texto não se comporta de forma idêntica nas duas, porém, e cada diferença é dita neste guia onde importa. O "z.number()" do Zod 4 rejeita um número infinito, enquanto o do Zod 3 o aceita, o que é o aviso de Infinity. Uma chave cujo valor é "z.unknown()" é opcional no tipo inferido do Zod 3 e obrigatória no do Zod 4 — e obrigatória também quando os dados são analisados, a partir do Zod 4.4 —, algo que esta saída nunca escreve senão além do limite de profundidade. O Zod 3 verifica uma chave com a grafia "__proto__" e o Zod 4 não. E o compilador desiste do tipo do Zod 4 para um array aninhado a dezenas de níveis de profundidade, enquanto calcula o do Zod 3.

Ela é escrita para o Zod comum, com métodos: "z.string().nullable().optional()". O Zod Mini escreve o mesmo esquema com funções em vez disso, "z.optional(z.nullable(z.string()))", então o Zod Mini não consegue rodar a saída tal como está.

Por que a raiz vem por último

A página de TypeScript imprime a raiz primeiro e os objetos que ela usa depois, já que um tipo pode ser usado antes da linha que o declara. Um esquema não pode: ele é um valor, e um "const" lido acima da própria declaração lança um ReferenceError enquanto o módulo ainda está carregando. Por isso todo esquema aqui vem depois de cada esquema que ele usa, e a raiz vem por último — Customer e LineItem primeiro no exemplo, depois o Root que os contém —, e é por isso que a raiz que abre a página de TypeScript fecha esta.

Uma ordem assim sempre existe. A inferência é uma árvore, cada objeto que ela extrai usado a partir de exatamente um lugar, então nenhum esquema precisa se referir a si mesmo ou a um impresso depois dele, e a saída nunca precisa de "z.lazy".

Perguntas frequentes

Por que um id é "z.number()" e não "z.number().int()"?
Porque uma amostra pode mostrar que todo id até agora foi inteiro, mas não que o próximo será. Em vez disso, a página o diz: o aviso de números inteiros lista cada posição em que todo número era inteiro, e, onde você sabe que um valor precisa continuar inteiro, adicionar ".int()" é uma edição de uma palavra. O aviso fica de fora onde um número é um inteiro fora do intervalo seguro, já que o ".int()" do Zod 4 rejeitaria a própria amostra.
Por que um endereço de e-mail sai como um simples "z.string()"?
Porque uma string que parece um e-mail na sua amostra não diz nada sobre a próxima, e uma verificação de formato correta precisa seguir os próprios padrões do Zod, que mudam entre versões — o "z.uuid()" do Zod 4 já rejeita strings que o ".uuid()" do Zod 3 deixava passar. Datas, URLs e UUIDs continuam strings pelo mesmo motivo, e um campo que nunca teve senão alguns valores nunca vira um enum ou um literal. Se você conhece a regra, escreva-a; o esquema é código comum que pertence a você.
Por que minha própria amostra falha no Zod 4?
Ela contém um número além do que um número do JavaScript consegue armazenar — 1e999, digamos —, que virou Infinity ou -Infinity quando o JSON foi analisado, e o "z.number()" do Zod 4 rejeita um número infinito, enquanto o do Zod 3 o aceita. O aviso de Infinity nomeia todas as posições envolvidas. Fora isso, um esquema sempre aceita a amostra de onde veio, já que cada valor da amostra entrou nele, e o repositório verifica isso nas duas versões com um corpus de amostras.
Por que um campo que eu enviei sumiu do resultado analisado?
Porque o esquema não o lista. O "z.object" aceita um objeto com chaves extras e o devolve sem elas, em qualquer das duas versões, e uma chave que faltou na sua amostra é uma chave que o esquema nunca aprendeu. Adicione-a ao esquema, ou torne permissivo aquele único objeto — "z.looseObject" no Zod 4, ".passthrough()" no Zod 3 — se chaves desconhecidas devem passar intactas.
Movi um esquema para baixo de outro e recebi um ReferenceError. Por quê?
Porque um esquema é um valor, e o JavaScript não deixa que um valor seja lido acima da linha que o define. A página imprime cada esquema depois de todos os esquemas que ele usa, com a raiz por último, exatamente por esse motivo; mantenha um esquema acima de tudo o que se refere a ele e o erro desaparece.
Por que o esquema se chama Customer e não CustomerSchema?
Essa é a própria convenção do zod.dev: um esquema e o tipo que ele infere compartilham um só nome, o que o TypeScript permite porque valores e tipos vivem em espaços de nomes separados. O nome em si é o que a página JSON para TypeScript dá ao mesmo objeto, então é a mesma palavra nas duas páginas, tanto para o esquema quanto para o seu tipo.
Para qual versão do Zod a saída é escrita?
Para o Zod 4, sem usar nada que falte ao Zod 3, então ela roda sem mudanças em qualquer um dos dois. Os dois diferem em alguns pontos — um número infinito, uma chave além do limite de profundidade, uma chave com a grafia "__proto__" e o tipo de um array aninhado muito profundamente — e cada um é explicado acima. É o Zod comum, com métodos encadeados; o Zod Mini escreve o mesmo esquema com funções e não consegue rodá-la tal como está.
Posso colar dados reais, com credenciais e tudo?
Sim. O esquema é calculado no seu navegador: o que você cola é lido na sua própria máquina e não é enviado a um servidor, nem armazenado, nem registrado. O esquema também não guarda nenhum dos seus valores, só suas chaves e o tipo de valor sob cada uma, então um token da amostra sai como "z.string()" e nada mais.

Ferramentas relacionadas

  • JSON para TypeScript

    Um esquema desta página roda como parte do seu código e verifica os dados toda vez que eles chegam. Aquela página dá a mesma resposta para o mesmo JSON na forma de simples tipos TypeScript, com os mesmos nomes, que são verificados quando seu código compila e não acrescentam nada em tempo de execução.

  • JSON para Go

    Esta página deixa para você exigir números inteiros onde todo número da sua amostra era inteiro. Aquela página calcula a mesma forma com os mesmos nomes de tipo, mas o Go não tem um tipo que seja simplesmente um número JSON, então, onde todo número está escrito como número inteiro, ela dá ao campo um tipo inteiro, e um valor escrito com ponto decimal não será decodificado nele.

  • Testador de JSONPath

    Teste consultas JSONPath (RFC 9535) em JSON.

  • Gerador de tabelas Markdown

    Crie e alinhe tabelas Markdown a partir de CSV, TSV ou JSON.