Todos los recursos

Deja de pedirle al modelo que "responda en JSON" y rezar para que salga bien. Restringe la salida con un esquema de verdad, un tool call forzado o structured outputs, y después valídala como si la hubiera escrito un desconocido, porque para tu parser eso es exactamente lo que pasó.

Obtén JSON confiable de Claude

En resumen

  • "Responde en JSON" en texto plano funciona casi siempre, y "casi siempre" es justo lo que te tumba el pipeline a las 2 de la mañana.
  • Dos caminos confiables: un tool call forzado (sirve en cualquier modelo) o structured outputs con «output_config.format» (Opus 4.8, Sonnet 4.6, Haiku 4.5).
  • Un esquema restringe la forma, no la verdad. El modelo puede devolverte un objeto con tipos impecables y datos completamente errados, así que valida siempre los valores.
  • Mantén los esquemas planos, ponles nombres descriptivos a los campos y marca como required solo los que de verdad necesitas.
  • Atiende los casos que no son tu camino feliz: refusals, truncamiento por «max_tokens» y campos que el modelo no logró llenar.

Pedirle al modelo que "responda en JSON" en texto plano te da JSON casi siempre. Las otras veces te cae un saludito de cortesía, un bloque de markdown pegado al final o una frase suelta pidiendo disculpas, y tu «JSON.parse» revienta en producción mientras duermes. Esta guía muestra las dos formas confiables de restringir la salida de Claude a una forma que tú controlas, recorre un extractor de facturas paso a paso y va al grano con la parte que todo el mundo se salta: un esquema garantiza la estructura, nunca la corrección. Vas a salir sabiendo cuál enfoque usar, cómo leer el resultado y qué fallas atender antes de que te despierten.

Nota

Los ejemplos usan el SDK de TypeScript de Anthropic y claude-opus-4-8. Todo se traslada 1:1 al SDK de Python; los nombres de los campos son idénticos. Los tool calls forzados sirven en cualquier modelo Claude actual. Structured outputs (el camino de «output_config.format») requiere Opus 4.8, Sonnet 4.6 o Haiku 4.5.

01 · Requisitos y por qué falla el JSON en prosa

Antes de meterte al código, deja esto listo. Son detalles pequeños, pero saltárselos es de donde salen casi todos los reportes de "a mí me funciona".

  • El SDK instalado. Corre npm install @anthropic-ai/sdk. Impórtalo como Anthropic desde @anthropic-ai/sdk.
  • «ANTHROPIC_API_KEY» en tu entorno, no quemada a mano en un string que vas a subir al repo. Crea el cliente sin argumentos, new Anthropic(), y deja que él lea la variable de entorno. Esta es la fuga de secretos más común que veo en proyectos nuevos.
  • Una idea clara de la forma que quieres. No "algunos campos de la factura," sino las llaves concretas, sus tipos y cuáles son innegociables. El esquema es esa idea puesta por escrito; lo que aquí dejes vago después se te devuelve como output inestable.

¿Por qué falla el enfoque en texto plano? Porque le estás pidiendo al modelo dos trabajos a la vez: razonar sobre el contenido y sostener un formato de serialización rígido en cada token. Casi siempre gana el razonamiento. Te queda la respuesta correcta envuelta en el sobre equivocado: un prefijo "Aquí está el JSON que pediste:", un bloque de código que el modelo agregó para ser útil, o una coma que se le fue en un array largo. Nada de eso es un error de razonamiento. Es ruido de formato, y la solución es dejar de pedirle al modelo que formatee. Que la API imponga el sobre para que el modelo solo tenga que llenar los valores.

02 · Camino A: un tool call forzado (sirve en cualquier modelo)

El truco confiable de toda la vida es definir una herramienta cuyo esquema de entrada sea la forma que quieres, y después obligar al modelo a llamarla. El modelo no puede responder con prosa, porque la única jugada que le queda es emitir argumentos que cumplan tu esquema.

import Anthropic from "@anthropic-ai/sdk"

const client = new Anthropic()

const res = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  tools: [
    {
      name: "extract_invoice",
      description: "Extrae campos estructurados de una factura.",
      input_schema: {
        type: "object",
        properties: {
          total: { type: "number", description: "Total general, solo numérico" },
          currency: { type: "string", description: "Código ISO 4217, ej. USD" },
          due_date: { type: "string", description: "Fecha ISO 8601, o vacío si no aparece" },
        },
        required: ["total", "currency"],
      },
    },
  ],
  tool_choice: { type: "tool", name: "extract_invoice" },
  messages,
})

const block = res.content.find((b) => b.type === "tool_use")
const data = block?.type === "tool_use" ? block.input : null

Tres cosas hacen que esto funcione, y cada una pesa:

  1. Pon tool_choice en { type: "tool", name: "extract_invoice" }. Esto fuerza esa herramienta exacta, así el modelo no puede responder en prosa ni escoger otra herramienta. Sin esto, el modelo decide por su cuenta si llama la herramienta o no, lo que está bien para un agente, mal para extracción.
  2. Marca como required cada campo del que de verdad dependes. Un campo que solo está como propiedad pero no como required puede quedar fuera sin que te enteres, y más adelante te llega undefined. Sé honesto sobre cuáles campos son opcionales; mira la sección 04.
  3. Lee el objeto estructurado desde input del bloque tool_use. El SDK ya lo parseó a un objeto de JavaScript, así que no le corras JSON.parse otra vez ni le hagas regex a la forma serializada. Primero acota el tipo del bloque (el array content es una unión) y después lee .input.

Consejo

Ponle una description de una línea a cada propiedad, no solo a la herramienta. El modelo las lee. "Código ISO 4217, ej. USD" reduce de forma medible las veces que recibes "$" o "dólares" donde querías "USD". Las descripciones son documentación gratis del esquema, y el modelo sí les hace caso.

03 · Camino B: structured outputs con «output_config.format»

En Opus 4.8, Sonnet 4.6 y Haiku 4.5 hay una ruta más directa. En vez de pedir prestado el mecanismo de tool call, le dices a la Messages API que restrinja la respuesta en sí a un esquema JSON. La respuesta de texto normal del modelo te llega ya cumpliendo tu forma, sin bloque de herramienta que andar escarbando.

La forma más limpia en TypeScript es client.messages.parse() con un esquema de Zod, que de paso te valida el resultado contra tu esquema.

import Anthropic from "@anthropic-ai/sdk"
import { z } from "zod"
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"

const Invoice = z.object({
  total: z.number(),
  currency: z.string(),
  due_date: z.string(),
  line_items: z.array(z.object({ label: z.string(), amount: z.number() })),
})

const res = await client.messages.parse({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  messages,
  output_config: { format: zodOutputFormat(Invoice) },
})

const data = res.parsed_output // tipado como el esquema de Zod — o null si el parseo falló

Unos cuantos detalles honestos para que esto no te agarre desprevenido:

  • «parsed_output» puede venir «null». Si el modelo se negó o la salida quedó cortada, no te llega un objeto tipado. Cúbrete con un guard; no lo des por sentado con ! y sigas de largo.
  • JSON Schema tiene sus límites aquí. Los esquemas recursivos y las restricciones de número o longitud (minimum, maxLength y compañía) no las impone la API. Los SDK de Python y TS quitan las restricciones no soportadas y las validan del lado del cliente, pero no asumas que el modelo respetó un minLength que pusiste; verifícalo tú.
  • La primera request con un esquema nuevo es más lenta. Hay un costo único de compilación; las llamadas siguientes con el mismo esquema pegan a un cache de 24 horas. No midas el rendimiento con la primera llamada.

¿Cuál camino elijo?

Usa structured outputs (Camino B) cuando estés en un modelo que lo soporta y la salida sea la respuesta: extracción, clasificación, una sola respuesta estructurada. Usa el tool call forzado (Camino A) cuando necesites soportar modelos viejos, cuando los datos estructurados son un paso dentro de un agente más grande que usa herramientas, o cuando quieras un mismo flujo de código sin importar si el modelo también llama otras herramientas. Ambos son confiables; ninguno es la lotería del texto plano.

04 · Un esquema restringe la forma, no la verdad

Esta es la sección que te ahorra un incidente de verdad, así que no la leas en diagonal. Un esquema te garantiza que vas a recibir un number en total. No te garantiza nada sobre si ese número es el total correcto. El modelo puede entregarte un objeto con tipos impecables donde currency dice "EUR" en una factura en USD, donde due_date es la fecha de emisión, o donde total es el subtotal antes de impuestos. Cada campo valida. Cada campo está mal.

Así que valida los valores, no solo los tipos, con chequeos que el esquema no es capaz de expresar:

function validateInvoice(data: { total: number; currency: string; due_date: string }) {
  const errors: string[] = []
  if (data.total <= 0) errors.push("el total debe ser positivo")
  if (!/^[A-Z]{3}$/.test(data.currency)) errors.push("currency debe ser un código ISO de 3 letras")
  if (data.due_date && Number.isNaN(Date.parse(data.due_date))) {
    errors.push("due_date no es una fecha parseable")
  }
  return errors
}

El modelo mental que te mantiene a salvo: trata la salida del modelo como si fuera input de un usuario en quien no confías. No escribirías un registro directo a tu base de datos solo porque un campo del formulario traía el tipo correcto. Primero verificarías que el valor tenga sentido. La salida del modelo merece la misma desconfianza. El esquema es tu validación de formulario para la forma; esta función es tu validación de reglas de negocio para el significado. Necesitas las dos.

Atención

Nunca conectes salida del modelo válida según el esquema directo a un efecto secundario, un pago, un write a la base de datos, un envío de correo, sin un chequeo de valores y, para todo lo destructivo, una persona de por medio. "Parseó" no es lo mismo que "está correcto". Un parseo limpio sobre un total equivocado es peor que un crash, porque no salta ninguna alarma.

05 · Maneja los caminos no felices antes de que te despierten

El camino feliz está a un «if» de quedar listo. Los caminos no felices son los que de verdad te sacan de la cama de madrugada. Tres que sí o sí tienes que manejar:

  1. Refusals. El modelo puede negarse por razones de seguridad y devolver stop_reason: "refusal". En el Camino B eso significa que parsed_output viene null; en el Camino A puede que ni siquiera haya bloque tool_use. Haz un branch: muestra el refusal, no reintentes el mismo prompt esperando una respuesta distinta.
  2. Truncamiento. Si stop_reason es "max_tokens", el modelo se quedó sin espacio a mitad de la estructura y tu JSON quedó incompleto. Para salidas grandes o anidadas, sube max_tokens y pásate a streaming. Cualquier cosa por encima de unos 16K tokens se arriesga a un timeout HTTP del SDK en una llamada sin streaming.
  3. Campos que el modelo de verdad no pudo llenar. No fuerces un valor donde no hay datos. Haz el campo opcional en el esquema y deja que el modelo lo omita, o dale una convención explícita de string vacío / null que dejes documentada en la description de la propiedad. Forzar un campo required que la fuente no trae es justo así como terminas con una fecha de vencimiento alucinada con toda la seguridad del mundo.

Un guard compacto que cubre los dos primeros:

if (res.stop_reason === "refusal") {
  throw new Error("El modelo se negó; no reintentar igual")
}
if (res.stop_reason === "max_tokens") {
  throw new Error("Salida truncada; sube max_tokens y usa streaming")
}

Arma bien el esquema, lee el campo estructurado en vez de hacerle regex a la prosa, valida los valores como si no confiaras en ellos, y maneja refusals y truncamiento de forma explícita. Ese es todo el trabajo. El modelo te entrega datos bien formados de manera confiable; todo lo demás en esta guía eres tú asegurándote de que "bien formado" también signifique "correcto," porque eso la API nunca te lo va a verificar.

Puntos clave

  • No pidas JSON en prosa; restringe la salida con un tool call forzado o structured outputs para que la forma la imponga la API, no las buenas intenciones del modelo.
  • Lee el campo estructurado que ya viene parseado («tool_use».input o «parsed_output»); nunca le corras «JSON.parse» otra vez ni le hagas regex al string serializado.
  • Un esquema garantiza la estructura, nunca la corrección; valida los valores como si fueran input de un usuario en quien no confías antes de cualquier efecto secundario.
  • Mantén los esquemas planos y bien descritos; marca como required solo los campos de los que de verdad dependes y deja que el modelo omita lo que la fuente no trae.
  • Siempre haz branch sobre «stop_reason»; maneja refusals y truncamiento por «max_tokens» de forma explícita en vez de reintentar a ciegas.

Preguntas frecuentes

¿Por qué no simplemente poner en el prompt "devuelve solo JSON válido, sin más texto"?

Porque funciona casi siempre, y un parser que falla el 2% de las veces es un parser que falla. El modelo de verdad intenta cumplir, pero un bloque de markdown colado, un "Claro, aquí tienes" al inicio, o una coma que se le fue en un array largo te rompen «JSON.parse», y ninguno es un error de razonamiento que puedas quitar con un prompt de forma confiable. Forzar un tool call o usar structured outputs traslada la garantía de las buenas intenciones del modelo a la API misma, que no anda con estados de ánimo.

Tool call forzado o structured outputs, ¿cuál uso por defecto?

Si estás en Opus 4.8, Sonnet 4.6 o Haiku 4.5 y el objeto estructurado ES la respuesta, ve por structured outputs por defecto («output_config.format», idealmente con «messages.parse» y Zod). Es el camino más directo y te valida solo. Usa un tool call forzado cuando necesites soportar modelos viejos, o cuando la extracción es un paso dentro de un agente más grande que también llama herramientas reales y quieres un solo flujo de código uniforme. Ambos son confiables; la decisión depende del soporte del modelo y de en qué parte de tu flujo cae el JSON.

La salida parseó sin problemas, ¿ya terminé?

No. Un parseo limpio significa que la forma está bien, no que los valores lo estén. El modelo puede devolverte una factura con tipos perfectos donde el total es el subtotal antes de impuestos y la moneda está mal. Valida siempre los valores con chequeos que el esquema no es capaz de expresar, como totales positivos, códigos ISO válidos, fechas parseables, y nunca dejes que una salida válida según el esquema dispare un pago, un delete o un correo sin un chequeo de valores y, para acciones destructivas, una persona de por medio.

¿Debería anidar mucho mi esquema para que calce con mi modelo de dominio?

Resístete. Los esquemas planos con nombres de campo descriptivos tienen una tasa de error mediblemente menor que los muy anidados o con nombres ambiguos. Si necesitas anidar (line items, direcciones), mantenlo poco profundo y dale a cada propiedad una description de una línea. Siempre puedes reacomodar después, en código, un resultado plano y confiable hacia tu modelo de dominio más rico. Ese mapeo es barato y determinista, mientras que llevar al modelo por cinco niveles de anidación no es ni lo uno ni lo otro.

¿Qué hago cuando «parsed_output» viene null o no hay bloque tool_use?

Revisa «stop_reason». Si es "refusal", el modelo se negó por razones de seguridad; muéstraselo al usuario y no reintentes el mismo prompt. Si es "max_tokens", la salida quedó truncada a mitad de la estructura; sube «max_tokens» y pásate a streaming (cualquier cosa por encima de ~16K tokens se arriesga a un timeout HTTP del SDK en una llamada sin streaming). Tratar un resultado null como "qué raro, reintento" sin leer el stop reason es justo así como quemas presupuesto en un loop que no tiene cómo salir bien.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados