Cómo Parsear JSON con IA sin Errores: Evitar Alucinaciones en OpenAI y Claude
¿Tu IA devuelve JSON roto o con campos inventados? Guía práctica para forzar respuestas JSON válidas con OpenAI, Claude y Zod. Con código Node.js listo.
Tabla de contenidos
Estás construyendo una feature que depende de la IA: extraer datos de un texto, clasificar contenido, generar un objeto estructurado. Todo funciona en tu prompt de prueba, pero en producción:
SyntaxError: Unexpected token 'C' at position 0
→ La IA respondió: "Claro, aquí tienes el JSON: {..."
O peor: el JSON es válido pero tiene campos inventados que rompen tu tipado.
Esto le pasa al 90% de los desarrolladores que integran LLMs. En este artículo te doy las 3 técnicas definitivas para obtener JSON perfecto de cualquier modelo de IA.
El problema real: por qué la IA rompe tu JSON
Los LLMs generan texto token a token. No “piensan” en estructuras de datos — predicen la siguiente palabra más probable. Esto causa:
| Problema | Ejemplo | Frecuencia |
|---|---|---|
| Texto antes del JSON | "Aquí tienes: { ... }" | Muy común |
| Markdown wrapping | json\n{...}\n | Común |
| Comillas simples | {'key': 'value'} | Ocasional |
| Campos inventados | {"nombre": "...", "sentimiento_astrológico": "..."} | Común |
| Campos faltantes | Omite campos que el prompt pedía | Ocasional |
| JSON truncado | Se corta a mitad por límite de tokens | Raro pero crítico |
Solución 1: Structured Outputs de OpenAI (la mejor)
Desde 2024, OpenAI soporta Structured Outputs — la respuesta está garantizada como JSON válido que cumple tu schema. No es un prompt trick: es una restricción a nivel de generación.
import OpenAI from 'openai';
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',
content: 'Extrae los datos del producto del texto del usuario.'
},
{
role: 'user',
content: 'El MacBook Pro M4 cuesta 1.999€, tiene 16GB RAM y pantalla de 14 pulgadas.'
}
],
response_format: {
type: 'json_schema',
json_schema: {
name: 'producto',
strict: true,
schema: {
type: 'object',
properties: {
nombre: { type: 'string', description: 'Nombre del producto' },
precio: { type: 'number', description: 'Precio en euros' },
specs: {
type: 'array',
items: { type: 'string' },
description: 'Lista de especificaciones'
},
disponible: { type: 'boolean' }
},
required: ['nombre', 'precio', 'specs', 'disponible'],
additionalProperties: false
}
}
}
});
const producto = JSON.parse(response.choices[0].message.content);
// ✅ TypeSafe: { nombre: "MacBook Pro M4", precio: 1999, specs: [...], disponible: true }
¿Por qué funciona al 100%? Con strict: true, OpenAI usa constrained decoding: el modelo solo puede generar tokens que producen JSON válido según tu schema. No es un “intento” — es una garantía.
Limitaciones
- Solo funciona con modelos
gpt-4.1,gpt-4.1-miniygpt-4.1-nano - El schema debe ser compatible con JSON Schema (no soporta
oneOfcomplejo) - Añade ~100ms de latencia en la primera petición (compila el schema)
Solución 2: Parseo robusto para Claude y modelos sin Structured Outputs
Claude (Anthropic) y modelos open-source no tienen Structured Outputs nativos. Necesitas un parser robusto que extraiga JSON aunque venga envuelto en texto.
/**
* Extrae y parsea JSON de una respuesta de IA,
* aunque venga envuelto en texto o markdown.
*/
function parseAIJson(raw) {
// 1. Intentar parseo directo
try {
return JSON.parse(raw);
} catch {
// Continuar con limpieza
}
// 2. Extraer de bloque markdown ```json ... ```
const markdownMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
if (markdownMatch) {
try {
return JSON.parse(markdownMatch[1].trim());
} catch {
// Continuar
}
}
// 3. Extraer el primer objeto/array JSON del texto
const jsonMatch = raw.match(/[\[{][\s\S]*[\]}]/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0]);
} catch {
// Continuar
}
}
throw new Error(`No se pudo extraer JSON válido de la respuesta: ${raw.slice(0, 200)}`);
}
Ejemplo con Claude
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: `Responde SOLO con un objeto JSON válido, sin texto adicional.
Schema: { "nombre": string, "precio": number, "specs": string[] }`,
messages: [
{
role: 'user',
content: 'Extrae datos: iPhone 16 Pro, 1.219€, chip A18 Pro, 256GB'
}
]
});
const producto = parseAIJson(message.content[0].text);
// ✅ { nombre: "iPhone 16 Pro", precio: 1219, specs: ["chip A18 Pro", "256GB"] }
Tip pro: Con Claude, añadir
Responde SOLO con JSONal system prompt reduce el texto extra en un 95%. Si necesitas prompts más efectivos, revisa la guía de mejores prompts para programar con IA.
Solución 3: Validación con Zod (la capa de seguridad)
Parsear JSON no es suficiente. Necesitas validar que los campos existen, tienen el tipo correcto y no hay campos inventados. Zod es el estándar para esto en TypeScript.
import { z } from 'zod';
// Define el schema UNA vez — sirve para validación y como tipo
const ProductoSchema = z.object({
nombre: z.string().min(1),
precio: z.number().positive(),
specs: z.array(z.string()).min(1),
disponible: z.boolean().default(true),
});
type Producto = z.infer<typeof ProductoSchema>;
function parseProductoIA(raw: string): Producto {
const parsed = parseAIJson(raw); // extrae JSON del texto
return ProductoSchema.parse(parsed); // valida y tipa
}
// Uso
try {
const producto = parseProductoIA(respuestaIA);
console.log(producto.nombre); // ✅ TypeSafe
} catch (error) {
if (error instanceof z.ZodError) {
console.error('La IA devolvió datos inválidos:', error.issues);
// → Reintentar con prompt más específico
}
}
Patrón completo: retry con validación
En producción, combinas las 3 técnicas con retry automático:
async function extractWithRetry<T>(
schema: z.ZodType<T>,
prompt: string,
maxRetries = 3
): Promise<T> {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: `Responde SOLO con JSON válido. Sin texto adicional.`,
messages: [{ role: 'user', content: prompt }]
});
const raw = response.content[0].text;
const parsed = parseAIJson(raw);
return schema.parse(parsed); // ✅ Válido
} catch (error) {
lastError = error as Error;
console.warn(`Intento ${i + 1} fallido:`, error.message);
}
}
throw new Error(`Fallo tras ${maxRetries} intentos: ${lastError?.message}`);
}
// Uso
const producto = await extractWithRetry(
ProductoSchema,
'Extrae datos: MacBook Air M4, 1.299€, 16GB RAM, 512GB SSD'
);
Si estás experimentando errores 429 durante los reintentos, consulta la guía de cómo solucionar el Error 429 en APIs de IA para implementar backoff exponencial.
Cuándo usar cada técnica
| Escenario | Técnica recomendada |
|---|---|
| Solo usas OpenAI (gpt-4.1) | Structured Outputs (strict: true) |
| Usas Claude o modelos open-source | Parser robusto + Zod |
| Multi-proveedor (OpenAI + Claude) | Structured Outputs en OpenAI, parser + Zod en el resto |
| Datos críticos (pagos, médicos) | Structured Outputs + Zod como segunda validación |
| Prototipo rápido | JSON.parse() + try/catch básico |
Errores comunes y cómo evitarlos
1. No definir additionalProperties: false
Sin esto, la IA puede inventar campos que pasan el parseo pero rompen tu lógica:
// ❌ Sin restricción
schema: { properties: { nombre: { type: 'string' } } }
// La IA puede añadir: { nombre: "X", opinion_personal: "Me encanta este producto" }
// ✅ Con restricción
schema: { properties: { nombre: { type: 'string' } }, additionalProperties: false }
2. Prompt demasiado vago
// ❌ Vago → la IA decide el formato
"Dame los datos del producto"
// ✅ Específico → formato predecible
"Extrae nombre (string), precio (number en €) y specs (array de strings)"
Para aprender a escribir prompts que produzcan output estructurado fiable, revisa los 20 mejores prompts para programar con IA.
3. No manejar JSON truncado
Si el max_tokens es bajo, el JSON puede cortarse:
// Detectar respuesta truncada
if (response.choices[0].finish_reason === 'length') {
throw new Error('Respuesta truncada: aumenta max_tokens');
}
Este problema está directamente relacionado con el error de context length exceeded: si tu prompt de entrada consume casi todo el límite, queda poco espacio para la respuesta.
Checklist para producción
- Schema definido con Zod (validación + tipos)
-
additionalProperties: falseen schemas de OpenAI - Parser robusto que maneja texto extra y markdown
- Retry automático (2-3 intentos) con backoff
- Detección de respuesta truncada (
finish_reason) - Logging de respuestas inválidas para mejorar prompts
- Fallback a modelo alternativo si falla el principal
Si estás construyendo un sistema que integra la IA con APIs externas, el function calling es una alternativa más robusta que parsear JSON manualmente.
Conclusión
El JSON roto es el error más común al integrar LLMs, pero tiene solución:
- OpenAI: usa Structured Outputs — JSON garantizado
- Claude y otros: parser robusto + prompt estricto
- Siempre: valida con Zod antes de usar los datos
La combinación de estas tres capas elimina el 99.9% de los errores de parseo en producción. Si quieres probar esto con APIs gratuitas antes de pagar, revisa cómo usar la API de ChatGPT y Claude gratis.