Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
A veces «dedúcelo de una muestra» no basta
Este sitio ya cuenta con un conversor JSON to Zod: pegue una respuesta de API y deducirá un esquema de Zod a partir de los valores.
Ese enfoque tiene un límite de fondo. Un valor de ejemplo no puede decirle si un campo es obligatorio. Ante {"nickname": "alice"} no hay forma de saber si nickname siempre está presente o si simplemente aparecía esta vez. Por la misma razón, "[email protected]" no indica que el campo deba ser un correo electrónico.
JSON Schema declara todo eso desde el principio.
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"email": { "type": "string", "format": "email" },
"nickname": { "type": "string" }
},
"required": ["id", "email"]
}

El conversor JSON Schema ⇄ Zod traslada esa información en lugar de descartarla, y también ejecuta la conversión inversa, generando JSON Schema a partir del código Zod que ya escribió. Como la sección components.schemas de OpenAPI es JSON Schema, puede pegarla directamente.
La gran trampa: required y .optional() tienen valores por defecto invertidos
Las dos especificaciones discrepan sobre qué significa el silencio.
| Cómo se expresa «obligatorio» | Si no dice nada | |
|---|---|---|
| JSON Schema | Listar el nombre en el array required | Opcional |
| Zod | Simplemente omitir .optional() | Obligatorio |
JSON Schema es «opcional salvo que se declare obligatorio»; Zod es «obligatorio salvo que se declare opcional». Si lo invierte, obtendrá un tipo en el que todos los campos que deberían ser obligatorios son opcionales.
El conversor absorbe la diferencia. Hacia Zod, solo las propiedades ausentes de required reciben .optional():
const required: string[] = Array.isArray(schema.required) ? schema.required : [];
const lines = Object.entries(properties).map(([key, value]) => {
let field = schemaToZod(value, depth + 1, warnings);
if (!required.includes(key)) field += '.optional()';
return `${indent}${formatKey(key)}: ${field},`;
});
En sentido inverso, las propiedades sin .optional() se recogen en el array required. El esquema anterior se convierte en:
import { z } from 'zod';
export const schema = z.object({
id: z.number().int(),
email: z.string().email(),
nickname: z.string().optional(),
});
export type Schema = z.infer<typeof schema>;
El alias z.infer se emite junto al esquema, de modo que la validación y el tipo estático nacen del mismo sitio.
Correspondencia de restricciones
| JSON Schema | Zod |
|---|---|
"type": "integer" | z.number().int() |
"format": "email" | z.string().email() |
"format": "date-time" | z.string().datetime() |
"minLength": 3 | z.string().min(3) |
"minimum": 0 | z.number().min(0) |
"enum": ["a","b"] (solo cadenas) | z.enum(["a", "b"]) |
"enum" (tipos mezclados) | z.union([z.literal(...), ...]) |
"type": ["string","null"] | z.string().nullable() |
"additionalProperties": false | .strict() |
oneOf / anyOf | z.union([...]) |
enum necesita una bifurcación: z.enum() de Zod admite solo cadenas, así que un enum con números o null no puede pasarse tal cual y se convierte en una unión de literales.
El lado Zod se analiza, nunca se ejecuta
La dirección inversa tiene una restricción. Bibliotecas como zod-to-json-schema reciben un objeto Zod en tiempo de ejecución y lo inspeccionan. Todas las herramientas de este sitio funcionan íntegramente en el navegador, lo que significa que el código pegado nunca puede pasar por eval.
Por eso el código Zod se analiza como texto. Una expresión como z.string().min(1).optional() se descompone en el nombre de la llamada (string), sus argumentos y la cadena de métodos (min(1), optional()).
Los paréntesis y los literales de cadena son la parte delicada. Buscar el siguiente ) falla con el anidamiento, y un valor como z.literal('a,b') rompe una división ingenua de campos. Por eso el emparejador de paréntesis lleva cuenta de si está dentro de una comilla:
const findClosing = (source: string, openIndex: number): number => {
let depth = 0;
let quote: string | null = null;
for (let i = openIndex; i < source.length; i += 1) {
const char = source[i];
if (quote) {
if (char === '\\') i += 1; // saltar el carácter escapado
else if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'" || char === '`') { quote = char; continue; }
if (char === '(' || char === '[' || char === '{') depth += 1;
else if (char === ')' || char === ']' || char === '}') {
depth -= 1;
if (depth === 0) return i;
}
}
return -1;
};
La misma idea se aplica al separar los campos de un objeto: solo cuentan las comas de nivel superior, así que z.object({ label: z.literal('a,b') }) sobrevive intacto.
El análisis empieza en el primer z., de modo que una copia que incluya import { z } from 'zod'; y export const user = sigue funcionando.
Lo que no se puede convertir nunca se descarta en silencio
$ref: las referencias no se resuelven, así que esa posición pasa a serz.any()allOf: no se puede expresar una intersección, por lo que solo se convierte el primer subesquemaif/then/not: no soportados
Convertirlos en z.any() sin avisar sería el peor resultado: la conversión parece limpia mientras la validación deja pasar todo. En su lugar se informan como advertencias bajo el resultado.
Lo mismo ocurre con funciones arbitrarias como .refine(), que JSON Schema no puede expresar y que se pierden en la conversión inversa. Trate el resultado como punto de partida de una migración y revíselo.
Cuándo usarlo
Si su proyecto parte de una definición OpenAPI, pegar components.schemas le da los tipos y la validación del front-end de una sola vez. Si Zod llegó primero en su código, ejecute la conversión inversa para generar JSON Schema para la documentación de su API.
Cuando lo único que tiene es una muestra, el conversor JSON to Zod sigue siendo la herramienta adecuada, y JSON a OpenAPI cubre la construcción del propio esquema OpenAPI. Todas funcionan en su navegador: el esquema que pega no se envía a ningún sitio.
Preguntas frecuentes
¿Debo usar esta herramienta o JSON to Zod?
Use esta cuando ya tenga un JSON Schema o una definición OpenAPI, porque se conservan los campos obligatorios, los enums y los formatos de cadena, y el esquema Zod resultante es más preciso. Si solo dispone de un valor de ejemplo, como una respuesta de API, use el conversor JSON to Zod.
¿Qué hago con los esquemas que contienen $ref?
Las referencias no se resuelven, así que esa posición pasa a ser z.any() y aparece una advertencia. Si su esquema extrae definiciones comunes a $defs, incorpórelas antes de pegar o corrija esa parte del resultado a mano.
¿Puedo usar el esquema Zod generado en producción tal cual?
Funciona como esqueleto, pero las reglas de negocio que nunca estuvieron en el JSON Schema no aparecerán. Además, pattern se convierte en z.string().regex(), que puede comportarse de otro modo si la expresión regular original no es compatible con ECMAScript. Revise el resultado antes de confiar en él.
¿Se envía a algún sitio el esquema que pego?
No. El conversor JSON Schema ⇄ Zod funciona íntegramente en su navegador y el lado Zod se analiza como texto en lugar de ejecutarse, así que las definiciones internas de su API nunca salen de su equipo.