JSON a Zod

Genera esquemas de Zod desde JSON, cada uno con su tipo z.infer: elementos de array fusionados, claves opcionales y nullable marcadas, nada adivinado.

Entrada
Esquema de 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>

Sintaxis de Zod 4 que también es válida en Zod 3. z.object descarta toda clave que no enumera, así que un campo que faltaba en tu ejemplo desaparece sin aviso de los datos que analiza.

  • En cada posición de abajo, tu ejemplo solo contenía null, así que el esquema no acepta nada más ahí.

    Posiciones: 1

    • Customer.phone
  • En cada posición de abajo, todos los números eran enteros. z.number() también acepta 7.5, y .int() exigiría números enteros, así que añádelo tú mismo solo donde sepas que un valor debe ser entero.

    Posiciones: 1

    • LineItem.quantity
  • En cada posición de abajo no se pudo inferir nada, así que el esquema no comprueba lo que hay ahí: z.unknown() acepta cualquier valor, y z.record(z.string(), z.unknown()), cualquier objeto.

    Posiciones: 1

    • Root.tags[]

Una comprobación que se ejecuta cada vez que llegan los datos

Un tipo de TypeScript se comprueba cuando tu código se compila y ya no existe cuando se ejecuta. Un esquema de Zod es la parte que se queda: se ejecuta dentro de tu programa y comprueba cada payload a medida que llega — una respuesta de una API, el cuerpo de un webhook, un mensaje sacado de una cola — antes de que tu código dependa de él. Pega un ejemplo de ese JSON y esta página escribe los esquemas por ti, listos para pegar en un módulo: cada objeto que tiene claves se convierte en un "z.object" con nombre, los elementos de un array fusionados en uno, y cada esquema va seguido del tipo de TypeScript que produce.

Es la respuesta que da la página JSON a TypeScript para el mismo JSON, escrita como validador en vez de como tipos. Las dos páginas leen una misma inferencia, así que se decide una vez, y solo se escribe dos veces, qué claves son opcionales, qué valores se vuelven una unión, dónde se mantiene null aparte de una clave ausente y cómo se llama cada objeto anidado. Esas reglas de fusión son el tema de la guía de la página JSON a TypeScript y aquí no se vuelven a contar. Esta guía trata de lo que hace el esquema con datos reales una vez que se ejecuta, y de lo que deja para que decidas tú.

Qué se acepta y qué se rechaza

Un esquema de esta página acepta el ejemplo a partir del cual se escribió, en Zod 3 y en Zod 4 por igual, salvo en el único caso que tiene un aviso propio, un número infinito en Zod 4. Más allá de ese ejemplo, acepta todo lo que se mantenga dentro de lo que dice el esquema, lo que, con los datos que de verdad vas a recibir, queda así:

  • Antes de llegar al límite de profundidad, una clave escrita sin ".optional()" es obligatoria. Un payload que la omite se rechaza, y también uno que pone ahí una clase de valor que el esquema no nombra — una cadena donde dice "z.number()", un objeto donde dice "z.string()".
  • Una clave escrita con ".optional()" puede faltar, y una escrita con ".nullable()" puede contener null. Ninguna de las dos deja pasar ninguna otra clase de valor, así que una clave opcional que está presente sigue teniendo que contener lo que nombra el esquema.
  • Un "z.union" admite cualquiera de sus miembros y nada más. Un "z.array" admite cualquier número de elementos, incluido ninguno en absoluto, siempre que cada uno encaje en el esquema escrito para sus elementos.
  • Donde el ejemplo no mostró nada — los elementos de un array vacío, un objeto sin claves, un valor anidado más allá del límite de profundidad —, el esquema no comprueba lo que hay ahí: admite cualquier valor, o cualquier objeto donde el objeto del ejemplo no tenía claves, y un aviso dice dónde.

Una clave que el esquema no enumera se deja pasar y luego se descarta del resultado; eso es el descarte, y tiene su propia sección. Una clave queda fuera de todo esto. Una clave llamada "__proto__" se escribe como clave calculada, entre corchetes, porque escrita tal cual fijaría el prototipo del objeto en el que está en vez de nombrar una clave — y, sin embargo, ninguna de las dos versiones de Zod devuelve esa clave en lo que entrega un análisis, y Zod 4 no comprueba su valor en absoluto.

Por qué el esquema nunca se vuelve más estricto por sí solo

Todo ejemplo tiene cosas en común que no puede demostrar: cada id un número entero, cada dirección de correo con forma de dirección de correo, un rol que nunca dijo más que admin. Eso es una regularidad, y una regularidad no es una comprobación. Un generador tiene la tentación de escribir una igualmente — ".int()" en los id, "z.email()" en las direcciones, un literal en el rol — y este no lo hace nunca, porque un ejemplo muestra lo que tus datos pueden contener y nunca lo que deben contener. Un tipo más estricto que tus datos cuesta un error de compilación en tu propia máquina. Un esquema más estricto que tus datos cuesta una petición rechazada en producción: el primer rol que no es admin, el primer id de 7.5, la primera dirección que el patrón no previó.

Además, las comprobaciones se moverían bajo tus pies. El "z.uuid()" de Zod 4 rechaza cadenas con forma de UUID que el ".uuid()" de Zod 3 aceptaba, porque comprueba los bits de variante, y el ".int()" de Zod 4 rechaza un entero fuera del rango seguro que el de Zod 3 dejaba pasar — así que una comprobación adivinada a partir del ejemplo de hoy sería una comprobación distinta según qué Zod instales. Por tanto, nada de lo que el ejemplo solo sugiere se escribe en el esquema. Donde importa cuando llegan los datos, la página te lo dice en su lugar en un aviso, y el cambio queda en tus manos.

Lo que el esquema no dirá por sí mismo

Debajo del esquema, la página enumera lo que notó y no escribió dentro: un aviso por cada clase de las de abajo que corresponda, dado una vez con todas las posiciones en las que se cumple. Una posición se escribe como la salida nombra las cosas — "Customer.phone" para una clave, "Root.tags[]" para los elementos de un array, la clave entre comillas y corchetes cuando no es un identificador y para "__proto__" —, de modo que se puede encontrar en el esquema de un vistazo. El ejemplo que se carga con la página muestra todas las clases salvo la de Infinity.

  • Solo null. El ejemplo nunca contuvo otra cosa que null en esa posición, así que el esquema dice "z.null()" y rechaza el primer valor real. Decide qué contiene el campo cuando se rellena y escríbelo tú mismo — "z.string().nullable()", por poner un caso — o pega un ejemplo en el que tenga un valor.
  • Infinity. Un número que supera lo que puede contener un número de JavaScript, como 1e999, se convierte en Infinity o -Infinity al analizar el JSON. El "z.number()" de Zod 4 rechaza un número infinito y el de Zod 3 lo acepta, así que en Zod 4 este es el único caso en el que un esquema rechaza el mismo ejemplo a partir del cual se hizo. La pregunta que plantea es sobre los datos y no sobre el esquema: las cifras se pierden antes de que ningún validador las vea, así que si ese valor debería ser un número en absoluto es algo que le corresponde responder a lo que lo escribió.
  • Números enteros. Todos los números de esa posición eran enteros, y "z.number()" también acepta 7.5. Donde un valor deba ser entero — un id, un recuento, una cantidad —, añade ".int()" tú mismo; donde sea un precio que por casualidad era redondo, déjalo como está. El aviso se omite en toda posición que contenga un entero fuera del rango seguro, de "Number.MIN_SAFE_INTEGER" a "Number.MAX_SAFE_INTEGER", porque el ".int()" de Zod 4 los rechaza, y un consejo que haría que un esquema rechazara su propio ejemplo es la única clase de consejo que la página no dará.
  • Nada que inferir. Los elementos de un array vacío, un objeto sin claves y un valor anidado a más de 100 niveles de profundidad no dan a la inferencia nada en que basarse, así que el esquema no comprueba lo que hay ahí: "z.unknown()" acepta cualquier valor, sea cual sea, y "z.record(z.string(), z.unknown())", cualquier objeto. Pega un ejemplo en el que ese array tenga elementos y ese objeto tenga claves, o escribe esa parte del esquema a mano.

Un aviso nunca cambia un byte del esquema. Es una frase a su lado, en el idioma de la página, y el cambio al que apunta es tuyo, para hacerlo o para omitirlo. No hay ningún aviso sobre formatos de cadena, literales ni enums: cada uno sería adivinar una regla que el ejemplo no puede mostrar.

Las claves que el esquema no enumera se descartan

"z.object" deja pasar un objeto que lleva claves que no enumera y las omite de lo que devuelve. Es el comportamiento predeterminado de Zod en las dos versiones, y lo más silencioso que hace un esquema: un campo que por casualidad faltaba en tu ejemplo desaparece de los datos que recibe tu código, sin ningún error que lo diga. Por eso lo menciona la frase que hay debajo de cada esquema en esta página.

La página no elige por ti entre estricto y laxo, porque cada uno afirma más de lo que un ejemplo puede mostrar. Un objeto estricto rechaza cualquier clave que no enumera — ninguna clave salvo estas —, y ningún ejemplo puede demostrar eso del siguiente payload. Un objeto laxo conserva las claves de más, y su tipo inferido gana una firma de índice para ellas, así que dejaría de ser la respuesta de la página de TypeScript. El descarte es el único comportamiento que admite lo que admite el tipo de TypeScript y aun así infiere ese tipo — un valor con propiedades de más también satisface una interfaz. Para elegir otra cosa, cambia el esquema a mano:

  • Para rechazar las claves que el esquema no enumera, escribe "z.strictObject" donde la salida escribe "z.object" en Zod 4, o encadena ".strict()" al "z.object" en Zod 3.
  • Para conservarlas, escribe "z.looseObject" en Zod 4, o encadena ".passthrough()" en Zod 3. Zod 4 sigue ejecutando esos dos métodos de Zod 3, y los califica de heredados.

Cada objeto de la salida es un esquema propio, así que la elección se hace objeto por objeto: hacer estricta la raíz no cambia nada de los objetos anidados dentro de ella, lo cual a menudo es lo que quieres cuando solo te corresponde exigir el envoltorio exterior.

Un nombre para cada esquema y su tipo

Cada esquema va seguido de su tipo — "export type Customer = z.infer<typeof Customer>" justo después de "export const Customer" —, que es como zod.dev escribe sus propios ejemplos: un mismo nombre para el valor que comprueba los datos y para el tipo que produce, ya que TypeScript mantiene los valores y los tipos en espacios de nombres separados. Ese tipo es el que imprime la página JSON a TypeScript para el mismo JSON, nombre por nombre y clave por clave, con las mismas claves opcionales, las mismas uniones y null en los mismos lugares — salvo las excepciones de abajo.

El repositorio comprueba esa promesa en vez de fiarse de ella: un corpus de ejemplos pasa por las dos páginas y luego por el compilador de TypeScript con cada versión de Zod, al que se le pregunta, para cada nombre, si los dos tipos son idénticos y si cada uno es asignable al otro. Donde responde otra cosa, el lugar está en lo profundo de un documento. Más allá del límite de profundidad, donde una clave contiene "z.unknown()", Zod 3 infiere esa clave como opcional mientras que la página de TypeScript la hace obligatoria. Y en Zod 4 el compilador se rinde con el tipo de un array anidado a decenas de niveles de profundidad, e informa del error TS2589 en la propia línea del tipo; el esquema de encima de esa línea sigue ejecutándose y sigue aceptando el ejemplo, y solo se pierde el tipo inferido.

Las dos comparaciones leen una clave opcional como lo hace TypeScript por defecto. Con "exactOptionalPropertyTypes", que está desactivado salvo que un proyecto lo active, el "z.infer" de una clave opcional también admite un undefined explícito, en cualquiera de las dos versiones de Zod, donde el tipo de la página de TypeScript no lo admite.

Escrito para Zod 4 y todavía válido en Zod 3

La salida se ciñe a lo que tienen las dos versiones principales — "z.object", "z.array", "z.union" sobre dos o más miembros, "z.string()", "z.number()", "z.boolean()", "z.null()", "z.unknown()", "z.record(z.string(), z.unknown())", ".optional()", ".nullable()" y "z.infer" —, bajo la línea de importación con la que empiezan los propios ejemplos de zod.dev. Nada de ella llegó con Zod 4 y nada de ella está obsoleto allí, así que un proyecto que no ha dejado Zod 3 puede pegarla tal cual.

Aun así, el mismo texto no se comporta de forma idéntica en las dos, y cada diferencia se dice en esta guía donde importa. El "z.number()" de Zod 4 rechaza un número infinito donde el de Zod 3 lo acepta, que es el aviso de Infinity. Una clave cuyo valor es "z.unknown()" es opcional en el tipo inferido de Zod 3 y obligatoria en el de Zod 4 — y obligatoria también al analizar los datos, a partir de Zod 4.4 —, cosa que esta salida solo escribe más allá del límite de profundidad. Zod 3 comprueba una clave llamada "__proto__" y Zod 4 no. Y el compilador se rinde con el tipo de Zod 4 para un array anidado a decenas de niveles de profundidad, mientras que resuelve el de Zod 3.

Está escrita para el Zod normal, con métodos: "z.string().nullable().optional()". Zod Mini escribe el mismo esquema con funciones en su lugar, "z.optional(z.nullable(z.string()))", así que Zod Mini no puede ejecutar la salida tal como está.

Por qué la raíz va al final

La página de TypeScript imprime primero la raíz y después los objetos que usa, ya que un tipo puede usarse antes de la línea que lo declara. Un esquema no: es un valor, y una "const" leída por encima de su propia declaración lanza un ReferenceError mientras el módulo aún se está cargando. Así que aquí cada esquema va después de cada esquema que usa, y la raíz va al final — Customer y LineItem primero en el ejemplo, luego el Root que los contiene —, razón por la cual la raíz que abre la página de TypeScript cierra esta.

Un orden así existe siempre. La inferencia es un árbol, en el que cada objeto que extrae se usa desde exactamente un lugar, así que ningún esquema necesita referirse a sí mismo ni a uno impreso después de él, y la salida nunca necesita "z.lazy".

Preguntas frecuentes

¿Por qué un id es "z.number()" y no "z.number().int()"?
Porque un ejemplo puede mostrar que todos los id hasta ahora eran enteros, pero no que el siguiente lo será. La página lo dice en su lugar: el aviso de números enteros enumera cada posición en la que todos los números eran enteros, y donde sepas que un valor debe seguir siendo entero, añadir ".int()" es un cambio de una palabra. El aviso se omite donde un número es un entero fuera del rango seguro, ya que el ".int()" de Zod 4 rechazaría ese mismo ejemplo.
¿Por qué una dirección de correo sale como un simple "z.string()"?
Porque una cadena que parece una dirección de correo en tu ejemplo no dice nada de la siguiente, y una comprobación de formato que sea correcta tiene que seguir los propios patrones de Zod, que cambian entre versiones — el "z.uuid()" de Zod 4 ya rechaza cadenas que el ".uuid()" de Zod 3 dejaba pasar. Las fechas, las URL y los UUID siguen siendo cadenas por la misma razón, y un campo que nunca contuvo más que unos cuantos valores nunca se convierte en un enum ni en un literal. Si conoces la regla, escríbela; el esquema es código corriente que es tuyo.
¿Por qué mi propio ejemplo falla en Zod 4?
Contiene un número que supera lo que puede contener un número de JavaScript — 1e999, digamos —, que se convirtió en Infinity o -Infinity al analizar el JSON, y el "z.number()" de Zod 4 rechaza un número infinito donde el de Zod 3 lo acepta. El aviso de Infinity nombra cada posición implicada. Aparte de eso, un esquema siempre acepta el ejemplo del que salió, ya que cada valor del ejemplo entró en él, y el repositorio lo comprueba en las dos versiones sobre un corpus de ejemplos.
¿Por qué desapareció del resultado analizado un campo que envié?
Porque el esquema no lo enumera. "z.object" acepta un objeto con claves de más y lo devuelve sin ellas, en cualquiera de las dos versiones, y una clave que le faltaba a tu ejemplo es una clave que el esquema nunca aprendió. Añádela al esquema, o haz laxo ese objeto concreto — "z.looseObject" en Zod 4, ".passthrough()" en Zod 3 — si las claves desconocidas han de pasar intactas.
Moví un esquema debajo de otro y obtuve un ReferenceError. ¿Por qué?
Porque un esquema es un valor, y JavaScript no deja que un valor se lea por encima de la línea que lo define. La página imprime cada esquema después de todos los esquemas que usa, con la raíz al final, exactamente por esa razón; mantén un esquema por encima de todo lo que se refiere a él y el error desaparece.
¿Por qué el esquema se llama Customer y no CustomerSchema?
Es la propia convención de zod.dev: un esquema y el tipo que infiere comparten un nombre, lo que TypeScript permite porque los valores y los tipos viven en espacios de nombres separados. El nombre en sí es el que la página JSON a TypeScript da al mismo objeto, así que es la misma palabra en las dos páginas, tanto para el esquema como para su tipo.
¿Para qué versión de Zod está escrita la salida?
Para Zod 4, sin usar nada que le falte a Zod 3, así que funciona sin cambios en cualquiera de las dos. Las dos difieren en unos cuantos puntos — un número infinito, una clave más allá del límite de profundidad, una clave llamada "__proto__" y el tipo de un array anidado muy profundamente — y cada uno se explica arriba. Es Zod normal con métodos encadenados; Zod Mini escribe el mismo esquema con funciones y no puede ejecutarla tal como está.
¿Puedo pegar datos reales, con credenciales y todo?
Sí. El esquema se calcula en tu navegador: lo que pegas se lee en tu propia máquina y no se envía a un servidor, ni se almacena ni se registra. El esquema tampoco contiene ninguno de tus valores, solo tus claves y la clase de valor que hay bajo cada una, así que un token del ejemplo sale como "z.string()" y nada más.

Herramientas relacionadas

  • JSON a TypeScript

    Un esquema de esta página se ejecuta como parte de tu código y comprueba los datos cada vez que llegan. Esa página da la misma respuesta para el mismo JSON en forma de simples tipos de TypeScript, con los mismos nombres, que se comprueban cuando tu código compila y no añaden nada en tiempo de ejecución.

  • JSON a Go

    Esta página deja en tus manos exigir números enteros donde todos los números de tu ejemplo lo eran. Esa página calcula la misma forma con los mismos nombres de tipo, pero Go no tiene un tipo que sea simplemente un número de JSON, así que, donde todos los números están escritos como enteros, le da al campo un tipo entero, y un valor escrito con punto decimal no se decodificará en él.

  • Probador de JSONPath

    Prueba consultas JSONPath (RFC 9535) contra JSON.

  • Generador de tablas Markdown

    Crea y alinea tablas Markdown desde CSV, TSV o JSON.