
Salidas estructuradas con Vercel AI SDK: generateObject + Zod + Claude (TypeScript)
Define un esquema Zod y llama a generateObject({ model: anthropic('claude-sonnet-5'), schema, prompt }) del Vercel AI SDK: recibes un objeto totalmente tipado que cumple el esquema, con reintento y reparación automáticos si la salida es inválida, sin parseo frágil de JSON. Usa streamObject para transmitir objetos parciales a una UI progresiva.
El problema: parsear texto libre de un LLM es frágil
Define un esquema Zod y pásalo a generateText del Vercel AI SDK como output: Output.object({ schema }), con model: anthropic('claude-sonnet-5'). Recibes de vuelta un valor output totalmente tipado que el SDK ya validó contra el esquema, sin parseo frágil de JSON ni limpieza con regex. Para una UI progresiva, llama a streamText con la misma opción output y lee partialOutputStream conforme se va llenando.
Aquí está el dolor que esto elimina. Le pides a un LLM que “regrese JSON”, y luego escribes el mismo código sucio de siempre: JSON.parse() dentro de un try/catch, un regex para quitar las cercas de markdown, un loop de reintentos cuando el parseo truena, y aun así te llega una alerta cuando un campo salió como null o como string en lugar de número. Cada paso es código que mantienes y que se rompe en producción con el peor reporte de bug de tu cola.
El problema de fondo es que el texto libre no tiene garantías: el modelo puede envolver el JSON en prosa, meter una coma final o inventar un campo. Tu parser corre después de la generación, así que se entera del desastre demasiado tarde. Quieres que la forma esté garantizada antes de que tu código toque el resultado.
La solución: generación restringida por esquema (defines la forma y la recibes)
El giro mental: en lugar de parsear lo que sea que el modelo escupa, declaras la forma exacta que necesitas y el SDK se encarga de que la respuesta la cumpla. Defines el esquema con Zod, se lo pasas a Output.object, y de vuelta obtienes un output ya tipado en TypeScript. Sin JSON.parse, sin regex de reparación, sin adivinar tipos.
Pasan dos cosas gratis. Primero, TypeScript infiere el tipo del resultado directo de tu esquema Zod, así que output.total es number en compilación y tu editor autocompleta cada campo. Segundo, el SDK valida la salida contra el esquema antes de entregártela: si el modelo devuelve algo que el esquema rechaza, recibes un error tipado que atrapas en el punto de llamada, no un objeto roto tres funciones más abajo. Borras toda la capa de parsear-y-rezar.
Es la contraparte en TypeScript del modo de salidas estructuradas de Anthropic, que restringe la generación a nivel de token sobre la API de Mensajes cruda. La misma garantía de forma, pero con la ergonomía de una librería de esquemas que ya usas en tu app de Next.js.
Antes (prompt-and-parse)
- JSON.parse() dentro de try/catch
- Regex para quitar cercas y comas
- Loop de reintentos cuando el parseo truena
- Sin tipos: adivinas la forma en runtime
- Codigo fragil que mantienes tu
Despues (Output.object + Zod)
- Objeto tipado que cumple el esquema
- El SDK valida el resultado contra el esquema
- Los tipos salen del esquema Zod
- Error tipado si la salida no cabe
- Menos código, menos alertas a las 3 a.m.
Instalación: ai + @ai-sdk/anthropic + zod (AI SDK 7 es solo ESM)
El Vercel AI SDK es el toolkit de facto en TypeScript para apps con LLMs: chat, streaming, tool calling y salida estructurada, sobre muchos proveedores. La versión mayor actual es AI SDK 7 (lanzada el 25 de junio de 2026). La v7 está orientada a agentes y es solo ESM — requiere sintaxis import o archivos .mjs. require() de CommonJS ya no está soportado, así que pon "type": "module" en tu package.json.
Instalas el paquete central ai, el adaptador del proveedor y una librería de esquemas:
npm install ai @ai-sdk/anthropic zod
El proveedor de Claude es @ai-sdk/anthropic. Importas anthropic y lo llamas con un id de modelo. Lee ANTHROPIC_API_KEY del entorno por sí solo:
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-sonnet-5');
Si necesitas una configuración personalizada — baseURL, apiKey, headers o un fetch propio — usa createAnthropic en lugar de anthropic:
import { createAnthropic } from '@ai-sdk/anthropic';
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
Output.object + Zod: extraer datos estructurados a un objeto tipado
Aquí está el patrón completo. En AI SDK 7, la generación estructurada es parte del flujo de generateText: defines un esquema Zod, lo pasas como output: Output.object({ schema }), y desestructuras output de la respuesta. Ejemplo real: extraer los campos de un ticket a un objeto tipado.
import { generateText, Output } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { output } = await generateText({
model: anthropic('claude-sonnet-5'),
output: Output.object({
schema: z.object({
proveedor: z.string(),
total: z.number(),
moneda: z.string(),
fecha: z.string(),
}),
}),
prompt: 'Extrae los datos de este ticket: ...',
});
// output está tipado: output.total es number, output.proveedor es string
console.log(output.total);
output no es any. TypeScript infiere su forma del esquema Zod, así que output.total es number y output.proveedor es string sin que anotes nada. Si el modelo intenta devolver total como el string "1,160.00 pesos", el SDK lo detecta contra el esquema y te entrega un error tipado, no un objeto roto. El parseo frágil desapareció.
Nota sobre los nombres: en AI SDK 7 la salida estructurada vive en la opción output de generateText y streamText, no en funciones sueltas viejas. Al definir una tool, el esquema de entrada va en inputSchema o parameters — es otra ruta. Verifica el nombre exacto contra ai-sdk.dev al escribir, porque estos nombres se movieron entre versiones mayores.
Elige el modelo: anthropic(‘claude-sonnet-5’) frente a claude-opus-4-8
El id del modelo es solo un string que le pasas a anthropic(). Para la mayoría de las tareas de extracción y clasificación, claude-sonnet-5 es el balance correcto entre costo y capacidad:
const model = anthropic('claude-sonnet-5'); // balanceado
Cuando la tarea es más difícil — razonamiento sobre documentos ruidosos, esquemas anidados grandes, clasificación con muchas categorías sutiles — subes al más capaz:
const model = anthropic('claude-opus-4-8'); // el más capaz
Un aviso que te ahorra un bug tonto: no copies los ids de ejemplo del AI SDK (como claude-sonnet-4-20250514), porque suelen estar desactualizados. Verifica el id vigente contra la documentación de modelos de Claude antes de fijarlo. Un id viejo o inventado es la causa número uno de un 404 al primer intento.
Qué pasa cuando el modelo devuelve una salida inválida
Esta es la parte que hace que valga la pena la generación restringida por esquema en producción. La garantía es validación, no confianza ciega: el SDK empuja al modelo hacia tu esquema Zod y luego verifica el resultado contra él. Cuando la salida no cabe en el esquema — un campo faltante, un tipo equivocado, JSON malformado — el SDK no te entrega un objeto a medias. Lanza un error tipado NoObjectGeneratedError que carga el texto ofensor y su metadata, así atrapas una falla clara en el punto de llamada.
Lo que sí sigue siendo tu chamba: la salida cumple la FORMA, no necesariamente la VERDAD. Un objeto con la forma correcta todavía puede traer un total mal leído o un enum bien tipado pero elegido incorrectamente. La garantía es estructural, no semántica. Así que en tareas cerca del dinero valida los valores tú mismo — reconcilia totales, corre el checksum del RFC, mantén un default seguro en los enums. Es exactamente el mismo modelo mental que aplico con las salidas estructuradas de Claude: confía en la forma, valida la verdad.
streamText + Output.object: objetos parciales para una UI progresiva
Cuando quieres que la interfaz se vaya llenando conforme el modelo genera — en lugar de una pantalla en blanco mientras esperas el objeto completo — usas streamText con la misma opción output. Expone partialOutputStream, que transmite versiones parciales de tu esquema que puedes renderizar de inmediato.
import { streamText, Output } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { partialOutputStream } = streamText({
model: anthropic('claude-sonnet-5'),
output: Output.object({
schema: z.object({
titulo: z.string(),
resumen: z.string(),
etiquetas: z.array(z.string()),
}),
}),
prompt: 'Resume y etiqueta este documento: ...',
});
for await (const partial of partialOutputStream) {
// partial se va completando campo por campo
console.log(partial);
}
El patrón encaja con un formulario que se autocompleta o un panel que muestra los campos en cuanto están listos. Un caveat honesto: los parciales de este stream no se validan contra el esquema mientras se emiten — la validación aplica al resultado completo, no a cada parcial en vuelo. Renderiza los parciales para dar feedback, y confía solo en el output final para lo que cargue peso. Si tu app hace además chat con streaming de tokens o tool calling, revisa el post hermano sobre Vercel AI SDK con Claude: streaming y tool calling.
Enums, esquemas anidados y arreglos (clasificación + extracción)
El poder real aparece cuando combinas enums, objetos anidados y arreglos en un esquema — que es como se ven las tareas reales de clasificación y extracción. Un enum de Zod restringe un campo a un conjunto cerrado, así que el modelo no puede devolver una categoría fuera de tu lista:
const { output } = await generateText({
model: anthropic('claude-sonnet-5'),
output: Output.object({
schema: z.object({
categoria: z.enum(['factura', 'ticket', 'contrato', 'otro']),
prioridad: z.enum(['alta', 'media', 'baja']),
emisor: z.object({
nombre: z.string(),
rfc: z.string(),
}),
conceptos: z.array(
z.object({
descripcion: z.string(),
cantidad: z.number(),
importe: z.number(),
}),
),
}),
}),
prompt: 'Clasifica y extrae los datos de este documento: ...',
});
Con z.enum, output.categoria está garantizado dentro de tu conjunto — nada de categorías sorpresa pegándole a un switch. El objeto anidado emisor y el arreglo conceptos salen igual de tipados. Un caveat que importa: un enum garantiza que el valor está EN tu conjunto, no que sea el CORRECTO para este documento. Para enrutamiento de alto valor, mantén un default seguro como otro o flag_review y una ruta de revisión humana.
Cómo se relaciona con el modo de salida estructurada de Anthropic
Si ya leíste sobre las salidas estructuradas de Claude en producción, esto es la misma idea vista desde TypeScript. Anthropic tiene su propio modo a nivel de API que compila tu JSON Schema en una gramática y restringe la generación de tokens, garantizando la forma en la capa cruda de la API de Mensajes de Claude.
Output.object del AI SDK es la ergonomía en TypeScript sobre esa misma garantía: defines el esquema con Zod (que además te da tipos estáticos), el SDK valida la salida, y te queda un mismo patrón que funciona con muchos proveedores. Cuándo elegir cuál: usa la API cruda de Anthropic cuando quieres control total sobre la petición y ya trabajas en Python o contra el endpoint directo; usa el AI SDK cuando construyes en TypeScript/Next.js y quieres tipos, streaming y un router de proveedores en una sola librería. Si además necesitas un agente que decida qué hacer con lo extraído, el primer qué es un agente de IA queda un nivel arriba de esto.
Errores comunes: sobre-restringir, el requisito de ESM y verificar nombres al escribir
Tres tropiezos que veo una y otra vez, dichos de frente:
Sobre-restringir el esquema. Si marcas cada campo como requerido y prohíbes cualquier extra en documentos genuinamente ruidosos, obligas al modelo a inventar valores para huecos que no existen. Marca como opcional (z.string().optional()) lo que de verdad puede faltar, y deja que la validación de valores atrape lo demás. Un esquema demasiado apretado no es más seguro; empuja al modelo a alucinar.
El requisito de ESM. AI SDK 7 es solo ESM. Si tu proyecto todavía usa require(), tronará al importar. Pon "type": "module" en package.json y usa import. No es negociable en la v7.
Nombres que se movieron entre versiones. Este post está escrito para AI SDK 7, donde la salida estructurada vive en la opción output de generateText y streamText, no en funciones sueltas viejas. Fija “AI SDK 7” y verifica cada nombre — los helpers de Output, la propiedad de streaming, la ruta de useChat — contra ai-sdk.dev al escribir. No presentes un patrón de la v3/v4 como el actual.
Dónde usarlo: extracción de CFDI/tickets, autocompletar formularios y enrutado de peticiones
Los tres lugares donde esto paga solo:
Extracción de CFDI y tickets. Sacas rfc_emisor, total, iva y un arreglo conceptos de un ticket hecho un desastre, a un objeto tipado, para luego armar el CFDI. La forma queda garantizada; tú reconcilias los totales y validas el RFC antes de facturar — ingeniero, no contador: las reglas del SAT gobiernan la validez fiscal.
Autocompletar formularios. Con streamText y Output.object, los campos se llenan progresivamente conforme el modelo lee el documento. El usuario ve avance en vez de un spinner.
Enrutado de peticiones. Conviertes un mensaje de soporte o un payload de webhook en una decisión de enrutamiento tipada con un enum, en lugar de un switch frágil sobre strings crudos. Verifica siempre la firma del webhook antes de pasar nada a un modelo, y mantén un default seguro: nunca muevas dinero solo porque el modelo lo dijo.
- Define el esquema ZodCampos exactos, .describe() como pista, .optional() donde un valor pueda faltar
- Llama generateText / streamText con Output.objectmodel: anthropic('claude-sonnet-5') — verifica el id
- Confía en la FORMAobjeto tipado de vuelta, sin parseo; error tipado si no cabe
- Valida los VALOREStotales reconcilian, checksum del RFC, fechas plausibles
- Actúaarma el CFDI, autocompleta el formulario, enruta la petición
Sigue aprendiendo: streaming y tool calling, salidas estructuradas de Claude, RAG
Si estás decidiendo el stack completo, tengo guías sobre cuánto cuesta construir un MVP de SaaS y sobre autenticación para un SaaS mexicano. Y para el resto de la superficie del AI SDK:
- Post hermano: Vercel AI SDK con Claude — streaming y tool calling en Next.js, para chat con streaming de tokens y el loop de herramientas.
- Salidas estructuradas de Claude en producción: la garantía de forma en la capa cruda de Anthropic.
- Cómo usar la API de Claude: la API de Mensajes desde cero.
- Construye un sistema RAG con Claude: recuperación aumentada para respuestas fundamentadas.
- Y el resto de mis guías de agentes de IA para poner todo esto en producción.
Fuentes oficiales: documentación del AI SDK, proveedor de Anthropic para el AI SDK y el changelog de AI SDK 7.