Conclusión primero: un error de JSON.parse() casi nunca significa «el modelo no sabe escribir JSON». Significa que le diste al parser una respuesta de chat entera, y el parser solo acepta un valor JSON.JSON.parse acepta un único valor según la gramática JSON (ECMA-262 / RFC 8259). Las vallas Markdown, la prosa envolvente, las comas finales, los saltos de línea crudos, el truncamiento y el dialecto JS/Python lanzan SyntaxError al instante. El orden de arreglo en 2026 es: si puedes usar Structured Output o leer los arguments de Tool Calling, no parsees la prosa del chat; si tienes que parsear, extrae, luego parse, luego valida con JSON Schema — no empieces reparando con regex hasta que «más o menos parsea».
Escrito a fecha de 17 de septiembre de 2026. Este sitio ya tiene ¿Qué es Structured Output?, Cómo la IA genera JSON conforme a JSON Schema, OpenAI vs Gemini Structured Output, JSON estructurado con la API de Gemini y Por qué el Tool Calling depende de JSON Schema. Este texto solo responde por qué la salida del modelo no pasa JSON.parse, y en qué orden arreglarlo.
Qué acepta de verdad JSON.parse
En el navegador y en Node, JSON.parse implementa texto JSON, no «un literal de objeto JavaScript que se parece lo bastante». El espacio en blanco (espacio, tab, salto de línea, retorno de carro) puede rodear el valor. Fuera de eso, la entrada tiene que ser exactamente un valor: objeto, array, string, number, true / false / null. Cualquier no-blanco después de ese valor falla — Chrome suele decir Unexpected non-whitespace character after JSON.
Estas formas corren en JS y mueren en JSON. Los modelos las copian del corpus de entrenamiento todo el rato:
| Forma | Objeto JS / JSON5 | JSON.parse |
|---|---|---|
| Coma final | {"ok": true,} vale | lanza error |
| Comillas simples | {'ok': true} vale | lanza error |
| Comentarios | // note vale | lanza error |
| Claves sin comillas | {ok: true} vale | lanza error |
undefined / NaN / Infinity | existen en el lenguaje | lanza error |
| Salto de línea crudo en un string | los template strings lo permiten | lanza error; tiene que ser \n |
Depura con una pregunta: ¿le pasaste «un valor JSON» o «un párrafo que el modelo envolvió para que se lea bien»? El parser es dueño de lo primero. Lo segundo lo tienes que extraer tú.
Una tabla de clases de fallo
Clasifica el SyntaxError antes de pelearte con la redacción exacta. Chrome, Safari y Node dicen el mismo bug de formas distintas. Las clases son pocas:
| Clase | Qué suele emitir el modelo | Resultado típico | Haz esto primero |
|---|---|---|---|
| Envoltorio | vallas ```json, «aquí está el JSON» | El primer carácter no es { / [ | Quita la valla y recorta un valor balanceado |
| Dialecto | Comas finales, comillas simples, comentarios, claves sin comillas | Unexpected token | Pasa a Structured Output; no lo parsees como JS |
| Cadena rota | " sin escapar, saltos de línea crudos, comas de ancho completo | El string termina pronto, o no hay : tras una clave | Mira la columna; limita la longitud del campo |
| Truncamiento | Objeto o array sin cerrar | Unexpected end of JSON input | Sube el techo de salida; espera al stream |
| Varios valores | Dos valores JSON, o prosa tras el primero | Caracteres después del primer valor | Recorta solo el primer valor completo |
| Codificación | BOM, caracteres de ancho cero, doble stringify | Token raro, o el parse devuelve un string | Quita el BOM; mira typeof antes de volver a parsear |
En Agents, suma una más: los arguments de Tool Calling suelen ser ya un objeto, o un string JSON que el proveedor ya restringió. No pases el mensaje entero del assistant por JSON.parse. Es otro canal — ver por qué el Tool Calling depende de JSON Schema.
Vallas y prosa envolvente
Los modelos de chat están entrenados para meter el código en vallas. Aunque hayas escrito «solo JSON», la respuesta suele ser:
```json
{"ok": true, "id": "A-1024"}
```
Here is the result. I can explain the fields if you want.
El primer carácter es un backtick, no {. JSON.parse falla en la columna 0. Un «Claro, aquí está el JSON:» al inicio o un descargo al final es el mismo bug. Peor: dos valores — una muestra y luego el resultado de verdad. Si parseas el bloque entero, mueres después del primer }.
Extrae con una regla: encuentra el primer {} o [] balanceado (salta los corchetes dentro de strings) y pásale solo ese recorte a JSON.parse. Quita las vallas primero. No cortes con avaricia del primer { al último } — los corchetes dentro de strings, o un segundo objeto en la explicación, recortan mal.
Dialecto: comas finales, comillas simples, comentarios, claves sin comillas
Los modelos han visto masas de JavaScript, Python, JSON5 y YAML. Si les pides «datos estructurados», mezclan dialectos. Todo lo siguiente es ilegal para JSON.parse:
{
ok: true, // bare key + comment
'name': 'Ada', // single quotes
"tags": ["a",], // trailing comma
"flag": True // Python boolean
}
Suma undefined, NaN, Infinity, None. En sus lenguajes significan algo; JSON tiene null y números finitos. Cambiar JSON.parse por eval o new Function para «aceptar» estas formas convierte el parser en un sumidero de código arbitrario. No lo hagas en producción.
JSON5 y JSONC se tragan comentarios y comas finales. Vale para humanos que editan config. Es un mal parser por defecto para la salida de un modelo. En cuanto aflojas la gramática, ya no distingues «una coma de más» de «un string roto». Si necesitas una capa laxa, déjala detrás del fallo de extraer + parse, y sigue pasando Schema tras la reparación.
Cadenas y puntuación: escapes, saltos de línea, ancho completo y comillas tipográficas
Un string JSON legal usa comillas dobles. Las " interiores y las barras invertidas hay que escaparlas. Los caracteres de control tienen que ser \n, \t o \uXXXX. Cuando el modelo copia un comentario de usuario, las comillas y los saltos crudos caen dentro del campo. El string termina pronto; la siguiente coma o el siguiente carácter CJK se vuelve un token inesperado.
La salida CJK añade un conjunto sucio frecuente: coma de ancho completo ,, dos puntos de ancho completo : y comillas curvas “” / ‘’. Parecen puntuación; sus code points no son 0x2C / 0x3A / 0x22. Este «casi JSON» muere después del valor de name:
{
"name": "Ada",
"ok": true
}
No lo arregles con otra frase del tipo «por favor usa puntuación ASCII». Pon maxLength en los campos string largos, que el modelo cite el texto fuente en vez de reescribir la puntuación, y usa Structured Output en el canal final. Para depurar, pega el texto en el validador JSON y mira en qué columna se para el resaltado — una coma de ancho completo se ve al instante.
Truncamiento y streaming: Unexpected end of JSON input
Unexpected end of JSON input casi siempre significa que el texto se acabó antes que la gramática: falta }, falta ], o un string sin cerrar. En 2026 las fuentes habituales son un techo de tokens de salida, un corte de seguridad, o que llamaste a JSON.parse sobre un chunk incompleto del stream.
Una API en streaming te da deltas. Los primeros chunks pueden ser {"ok": tr. Si parseas eso, fallas. Haz esto:
- Espera a que el stream termine (
finish_reason/stop) y luego parsea el buffer completo; - O usa un parser JSON de streaming de verdad, que avanza token a token — no llames a
JSON.parsesobre medio valor; - Si el motivo de parada es
length/max_tokens, esto no es un bug de parseo. La generación no terminó — sube el techo, encoge el Schema o pagina el modelo.
Cerrar llaves a ciegas tras un truncamiento es un truco de borrador. La forma puede parsear y aun así faltar campos o partir un string por la mitad. Tras cualquier reparación, valida el Schema; si falla, reintenta. No lo guardes en silencio.
Caracteres invisibles y doble codificación
Un BOM UTF-8 (U+FEFF) no es espacio en blanco JSON. Algunos caminos de copia y algunos gateways lo ponen delante; JSON.parse entonces reporta un token inesperado en la columna 0. Los espacios de ancho cero y los guiones suaves hacen lo mismo. Quítalo con replace(/^\uFEFF/, "") y luego trim, antes de extraer.
La doble codificación es más silenciosa. Un JSON.stringify produce el string "{\"ok\":true}". Si parseas esa forma entrecomillada, obtienes el string {"ok":true}, no un objeto. Un segundo parse da el objeto. Si te paras tras un parse y lees .ok, obtienes undefined — «parseó» y no tiene campos. Mira typeof antes de volver a parsear. No dejes fijo «parsea siempre dos veces»; un objeto de verdad lanzará error.
Orden de arreglo: cambia el canal, luego extrae, repara al final
Este orden gana a apilar más frases en el prompt:
- Cambia el canal. Las respuestas finales van por Structured Output (OpenAI
response_format.json_schema, GeminiresponseMimeTypemás Schema, Claudeoutput_config.format). Los parámetros de herramientas van por losargumentsde Tool Calling, no por prosa raspada. Ver qué es Structured Output. - Extrae. Quita las vallas
```json; recorta el primer valor balanceado; tira el BOM. - Parsea en estricto. Usa solo
JSON.parse. Si falla, guarda el texto crudo y la posición del error. No hagaseval. - Valida el Schema. Un parse con éxito solo dice que la gramática es legal. Campos que faltan, tipos equivocados y claves de más necesitan JSON Schema / ajv. Ver cómo la IA genera JSON conforme a JSON Schema.
- Repara al final. Herramientas como
jsonrepairpueden cerrar llaves y quitar comas finales. Úsalas solo cuando extraer + parse fallen, y solo si aceptas que una reparación puede cambiar el sentido. Luego sigue ejecutando los pasos 3 y 4. No conviertas un reparador en el parser por defecto global.
Los prompts siguen ayudando: «sin vallas, sin explicación». Bajan las probabilidades de un envoltorio. No sustituyen un Schema, y no aflojan JSON.parse. En 2026, tratar la prosa del chat como una API significa que seguirás pagando vallas y truncamientos.
Un pipeline pequeño de extraer + parse
Un pipeline de tamaño didáctico: quita vallas, tira el BOM, recorta un valor balanceado y luego JSON.parse. Cubre los envoltorios habituales. No arregla comas finales ni puntuación de ancho completo — déjaselo a Structured Output o a una capa de reparación explícita.
function stripFence(text) {
const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
return m ? m[1] : String(text);
}
function sliceBalancedJson(text) {
const src = text.replace(/^\uFEFF/, "").trim();
const start = src.search(/[\{\[]/);
if (start < 0) throw new SyntaxError("No JSON value found");
const open = src[start];
const close = open === "{" ? "}" : "]";
let depth = 0, inStr = false, esc = false;
for (let i = start; i < src.length; i++) {
const ch = src[i];
if (inStr) {
if (esc) { esc = false; continue; }
if (ch === "\\") { esc = true; continue; }
if (ch === '"') inStr = false;
continue;
}
if (ch === '"') { inStr = true; continue; }
if (ch === open) depth++;
else if (ch === close) {
depth--;
if (depth === 0) return src.slice(start, i + 1);
}
}
throw new SyntaxError("Unterminated JSON value");
}
function parseModelJson(raw) {
return JSON.parse(sliceBalancedJson(stripFence(raw)));
}
El recortador tiene que saber si está dentro de un string, o un { en el valor de un campo cierra demasiado pronto. Los objetos y arrays anidados usan depth. Si el recorte sigue sin parsear, pega el texto fallido en el validador y usa la tabla de clases de arriba. No apiles más regex en esta capa.
Ver el error en local
No mandes la salida del modelo directo a un parser de producción. En el navegador, mira tres cosas: ¿es JSON legal?; si no, ¿en qué columna?; si ya tienes un Schema, ¿cumple el contrato?
- Validador JSON — mira dónde cae el
SyntaxError; adjunta un Schema cuando lo tengas. - Formateador JSON — si formatea, suele parsear; si falla, busca comas de ancho completo o vallas en el origen.
- JSON Diff — tras un parse con éxito, compara el objeto del modelo con el objeto mínimo que permites.
Nada sale del navegador. Encaja con una respuesta fallida del modelo, un Schema y un blob de arguments de Tool Calling, lado a lado. Estabiliza nombres de campo y required, luego cablea el Host.
FAQ
¿Por qué «parece JSON» y aun así falla JSON.parse?
El ojo humano tolera vallas, comas finales, comillas curvas y prosa envolvente. JSON.parse acepta exactamente un valor RFC 8259. Parecer JSON no es lo mismo que ser JSON legal.
¿Basta un regex que quite las vallas ```json?
No. Las vallas son solo un envoltorio. Siguen quedando prosa al final, un segundo valor JSON, comas finales y truncamiento. Tras quitar las vallas, recorta un valor balanceado y parsea en estricto.
¿En qué se diferencia JSON Mode de Structured Output?
JSON Mode suele restringir solo el «parece JSON», no campos ni tipos. Structured Output usa JSON Schema para bloquear tokens ilegales en el decode. Si un programa va a consumir el resultado, prioriza Structured Output. No actives JSON Mode y luego hagas JSON.parse del cuerpo del chat.
¿Deberían jsonrepair o JSON5 ser el parser por defecto?
No. Aceptan entradas que deberían fallar, y pueden cambiar el sentido. Úsalos solo como capa de reparación cuando extraer + JSON.parse fallen, y después sigue validando el Schema.
¿Cuándo puedo llamar a JSON.parse en una respuesta en streaming?
Cuando el stream haya terminado y el buffer sea un valor completo. Parsear medio chunk da Unexpected end of JSON input siempre. Para consumir tokens según llegan, usa un parser de streaming, no JSON.parse.
El parse funcionó pero los campos están mal. ¿Es este artículo?
Eso es la capa siguiente. JSON.parse solo garantiza gramática. Campos que faltan, tipos equivocados y claves de más son problemas de Schema — ver las piezas de Structured Output y de validación de Tool Calling en este sitio.
Resumen
Un fallo de JSON.parse es un problema de canal. Otra frase de «por favor, saca JSON» no lo arregla. Los modelos de chat envuelven vallas, mezclan dialectos y se paran en un techo de tokens. El parser acepta un valor JSON limpio. En 2026, cablea el modelo por Structured Output o por los arguments de Tool Calling; luego extrae + parse estricto + Schema; repara al final.
Los prompts pueden reducir envoltorios. No pueden aflojar la gramática. Pega el texto que falla en un validador local, mira en qué columna se para y decide: quita una valla, cambia el canal o sube el techo de salida. Los modelos cambian. Lo que acepta JSON.parse, y cuál es tu contrato de campos, no debería.