Skip to content
AI•4 min read

Structured outputs without suffering: JSON that doesn't break

En la demo el JSON sale bien. En producción, cuando el input es raro o la respuesta se alarga, sale casi bien. Y "casi" es un bug: un campo que falta a las 3 de la mañana es una página en rojo.

Structured outputs es la diferencia entre pedir JSON en el prompt y obligar al modelo a devolver una estructura válida. Esta pieza es cómo lo hago, con el código y la fuente.

El problema: "casi siempre JSON válido"

Si le pides JSON en texto libre, confías en que el modelo no se equivoque. Y se equivoca: comillas sin escapar, un campo de más, un número como string, un objeto truncado. El parseo falla, y el fallo aparece en el peor momento, no en tus pruebas.

La solución en 3 pasos

1. Define el esquema con zod (o el validador de tu lenguaje):

// Fuente: Structured Outputs with Vercel's AI SDK — AI Hero (adaptado)
import { z } from "zod";

const schema = z.object({
  ticket: z.object({
    title: z.string().describe("Un título corto y accionable"),
    category: z.enum(["billing", "bug", "cuenta", "otro"]),
    priority: z.enum(["alta", "media", "baja"]),
    summary: z.string().describe("Un resumen de una frase"),
  }),
});

2. Deja que el SDK pida la estructura, no el texto:

import { generateObject } from "ai";

const { object } = await generateObject({
  model,
  schema,
  schemaName: "Ticket",
  prompt: "Clasifica este mensaje del usuario: ...",
});

return object.ticket; // tipado, validado, listo

3. Usa el resultado con tipos. En TypeScript, object.ticket.priority es un union de tres valores: si el modelo devuelve otra cosa, no compila. - Source: Structured Outputs with Vercel's AI SDK - AI Hero

Las 3 claves que casi nadie usa

  1. .describe() en cada campo. No es decoración: es la instrucción de ese campo. "Un título corto y accionable" rinde más que title: z.string(). - Source: Structured Outputs with Vercel's AI SDK - AI Hero
  2. Enums en lugar de strings libres. Donde conoces las opciones, no dejes que el modelo invente: z.enum([...]) elimina categorías fantasma.
  3. schemaName. Un nombre ayuda al modelo a entender qué estructura está llenando (y mejora la traza cuando depuras).

Valida y reintenta (el seguro que falta)

Structured outputs reduce los fallos, no los elimina (ni en los modelos ni en las versiones). El patrón que uso: valida contra el esquema y, si falla, reintenta pasándole el error al modelo, con un máximo de 2 intentos y una salida segura si no lo logra.

for (let i = 0; i < 2; i++) {
  const { object } = await generateObject({ model, schema, prompt });
  const parsed = schema.safeParse(object);
  if (parsed.success) return parsed.data;
  prompt += `\nTu respuesta anterior falló: ${parsed.error.message}`;
}
return fallback; // nunca dejes al usuario con un crash

No dejes que el formato luche contra el modelo

Hay un detalle que Anthropic explica bien: algunos formatos son mucho más difíciles de escribir para un modelo que otros. Escribir un diff exige contar líneas; escribir código dentro de JSON exige escapar saltos de línea y comillas. Sus recomendaciones: dar suficientes tokens para pensar antes de escribir, mantener el formato cerca de lo que el modelo ve naturalmente en texto, y evitar overhead de formato como escapar o contar líneas. - Source: Building effective agents (Apéndice 2) - Anthropic

Traducción: si tu esquema obliga al modelo a hacer malabares de escape, no es un problema de JSON: es un problema de diseño de la interfaz.

Cuándo NO usar structured outputs

  • Texto largo o prosa (un post, una explicación): ahí la estructura es ruido.
  • Exploración abierta: cuando no sabes la forma de la respuesta todavía, primero prototipa en texto y luego fija el esquema.
  • Como sustituto de validación de negocio: que el JSON cumpla el esquema no significa que los datos sean correctos. Eso se mide con evals.

5 reglas

  1. Define el esquema antes del prompt. El contrato primero.
  2. .describe() cada campo. Es la instrucción que el modelo sí lee.
  3. Enums donde haya opciones cerradas.
  4. Valida y reintenta con el error incluido; máximo 2 intentos.
  5. Diseña el esquema para que sea fácil de escribir, no solo bonito para tu código.

Esto es la pieza de "interfaz" del harness aplicada a la salida; el marco está en Harness Engineering: las 9 piezas y el código en contexto en Agentes con Vercel AI SDK 7.

¿Tu app parsea texto libre o ya devuelve objetos validados? Cuéntame en los comentarios qué campo fue el que te rompió producción.

Referencias

> More posts