JSON no tiene comentarios, y fue a propósito
Los comentarios se quitaron de JSON para proteger la interoperabilidad, y esa decisión se la ha cobrado cada archivo de configuración desde entonces.
Cada afirmación de esta página está medida o tiene fuente. Cuando no es ninguna de las dos, lo dice.
Añades una línea a un archivo de configuración explicando por qué un timeout es de 45 segundos y no de 30, el despliegue falla, y el mensaje no ayuda nada:
JSON.parse('{\n // 45s: upstream p99 is 38s\n "timeout": 45\n}')
// Expected property name or '}' in JSON at position 4 (line 2 column 3)
El parser vio una barra donde debía ir una clave y se rindió. Python no es más claro sobre la causa:
json.loads('{\n // 45s: upstream p99 is 38s\n "timeout": 45\n}')
# JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)
Ninguno de los dos mensajes menciona los comentarios, porque en lo que respecta a la gramática no hay nada que mencionar. La RFC 8259 define exactamente cuatro caracteres que pueden aparecer entre tokens: espacio, tabulador, retorno de carro y salto de línea. Todo lo demás es o parte de un valor o un error de sintaxis.
Por qué se quitaron
Los comentarios estaban en las primeras versiones de JSON y Douglas Crockford los sacó. Su razón declarada es la parte interesante: la gente no los usaba para prosa, los usaba para transportar directivas de parseo. Algo así como una pista de codificación o un puntero a un esquema, escrito en un comentario, que un consumidor concreto leería y ejecutaría. Llegados a ese punto, el comentario ya no es un comentario. Es un segundo canal de datos, no documentado, viajando dentro de un formato cuyo argumento de venta entero era que cualquier parser en cualquier parte leería los mismos valores a partir de los mismos bytes.
La alternativa que el propio Crockford sugirió era pasar tu archivo comentado por un minificador antes de entregárselo a un parser. Sigue siendo la forma correcta de respuesta, y el resto de este artículo va sobre todo de hacerlo bien.
La decisión era defendible para lo que JSON era en sus primeros años: un formato de transporte para mover un valor entre dos programas que ya se habían puesto de acuerdo sobre su significado. Nadie comenta un paquete de red.
Por qué duele igualmente
JSON no se quedó en formato de transporte. Se convirtió en el lenguaje de configuración por defecto de toda la cadena de herramientas, y la configuración es justo el caso donde el razonamiento detrás de un valor importa más que el valor. Un retries: 0 sin explicación acaba «arreglado» por la siguiente persona, con toda su buena intención. Un retries: 0 con // intencionado, este endpoint no es idempotente encima, no.
Así que cada ecosistema que adoptó JSON para configuración se ha buscado su propio parche encima, y no son compatibles entre sí.
Las cinco opciones
Una clave _comment
{
"_comment": "45s porque el p99 de upstream es 38s",
"timeout": 45
}
Es JSON estricto, se parsea en todas partes y no necesita ninguna herramienta. Los problemas son reales, eso sí. Tu esquema ahora tiene que permitirlo o tu validador lo rechaza. Es un dato, así que se envía a los clientes, acaba en los logs, y aparece en los diffs como un cambio de valor y no como un cambio de comentario. Y tienes exactamente uno por objeto: la RFC 8259 dice que las claves DEBERÍAN ser únicas y deja los duplicados sin definir, con JavaScript y Python quedándose ambos con el último, así que un segundo _comment al mismo nivel se come al primero en silencio. La gente lo rodea con _comment1, _comment2, que es el punto en el que el enfoque deja de compensar.
Úsalo para una nota de cabecera al principio de un archivo. No lo uses para anotar línea a línea.
JSONC
JSONC es JSON más dos cosas: comentarios // y /* */, y comas finales. Nada más. Es lo que usa VS Code para sus propios settings.json y keybindings.json, y lo que TypeScript acepta en tsconfig.json.
{
// el p99 de upstream es 38s
"timeout": 45,
"retries": 0, // este endpoint no es idempotente
}
Conviene ser franco sobre su estatus: no hay ninguna especificación independiente de JSONC. No hay RFC, ni número de versión, ni suite de conformidad. Es una convención con una implementación con forma de editor detrás, y los dialectos varían en los bordes (si se admite una coma final tras el último elemento de un array, si los comentarios sobreviven a un ida y vuelta). Es la opción más segura cuando tu consumidor ya es una herramienta que lo soporta, y una mala opción para cualquier cosa que le entregues a un tercero.
JSON5
JSON5 sí es una especificación de verdad con historial de versiones, y se extiende bastante más allá que JSONC:
- Claves de objeto sin comillas, cuando la clave es un identificador ES5 válido
- Cadenas con comillas simples
- Comas finales en objetos y arrays
- Comentarios de línea y de bloque
- Números hexadecimales
- Puntos decimales al principio y al final, así que
.5y5.son números Infinity,-InfinityyNaN
El último punto es el que hay que pensarse a fondo. NaN y los infinitos no tienen ninguna representación en JSON, así que un documento JSON5 que los use no puede convertirse a JSON sin una decisión con pérdida sobre qué poner en su lugar. El resto de las extensiones son cosméticas y sobreviven bien a una conversión. Usa JSON5 cuando el autor principal del archivo sea una persona y una extensión .json5 sea aceptable; no lo uses como formato de API.
Deja de usar JSON
Si el archivo es configuración que controlas de punta a punta, y nada externo lo consume, el formato es una elección libre y JSON no es obviamente la mejor. YAML y TOML tienen comentarios de primera clase. Ambos tienen sus propios costes, y la comparativa merece una lectura antes de comprometerte, porque YAML en particular te entregará el problema de Noruega: bajo la semántica de YAML 1.1, que implementan PyYAML y el Psych de Ruby, un no sin comillas se parsea como el booleano false.
Elimínalos en tiempo de compilación
Mantén el archivo anotado como fuente de verdad, elimina los comentarios en CI, publica JSON estricto. Es la sugerencia de Crockford y encaja con todo: tus editores y revisores ven los comentarios, tu parser en tiempo de ejecución ve un documento que satisface la RFC 8259, y ningún consumidor necesita saber que existe ninguno de los dos formatos.
Eliminar comentarios sin romper las URLs
La implementación obvia es una expresión regular, y la expresión regular obvia está mal:
// No hagas esto.
text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
Ejecútala sobre esto y mira cómo destruye un valor:
{
"endpoint": "https://api.example.com/v2/orders", // producción
"note": "consulta /* el runbook */ antes de cambiarlo"
}
La primera regla encuentra // dentro de https:// y borra el resto de la línea, incluida la comilla de cierre y la coma. La segunda encuentra un comentario de bloque dentro de una cadena. Acabas con una cadena sin terminar y un error que apunta a un sitio que no tiene nada que ver. Una regex no puede hacer este trabajo porque no puede saber si una barra está dentro de una cadena, y el contexto de cadena en JSON depende de contar escapes.
Necesitas un escáner que siga exactamente un estado:
function stripJsonComments(text) {
let out = '';
let inString = false;
let inLine = false;
let inBlock = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
const next = text[i + 1];
if (inLine) {
if (c === '\n') { inLine = false; out += c; }
continue;
}
if (inBlock) {
// conserva los saltos de línea para que los números de línea sigan coincidiendo
if (c === '*' && next === '/') { inBlock = false; i++; }
else if (c === '\n') { out += c; }
continue;
}
if (inString) {
out += c;
if (c === '\\') { out += next; i++; continue; } // escape, consume los dos
if (c === '"') inString = false;
continue;
}
if (c === '"') { inString = true; out += c; continue; }
if (c === '/' && next === '/') { inLine = true; i++; continue; }
if (c === '/' && next === '*') { inBlock = true; i++; continue; }
out += c;
}
return out;
}
La rama del escape es la parte que la gente se deja. Sin ella, la comilla escapada de "dijo \"ve a https://example.com\" hoy" se lee como el final de la cadena, así que el // que viene después se toma por el inicio de un comentario y el resto de la línea desaparece.
Fíjate también en lo que esto no hace. Eliminar los comentarios deja atrás las comas finales, y esas fallan por su cuenta con su propio error: V8 reporta Expected double-quoted property name in JSON at position 7 (line 1 column 8) para {"a":1,}, y el completamente distinto Unexpected token ']', "[1,2,]" is not valid JSON para [1,2,]. Una conversión de JSONC a JSON tiene que manejar ambos.
Si prefieres no cargar con el escáner, pega el archivo en Reparar JSON, que quita comentarios y comas finales en una sola pasada y te devuelve JSON estricto, y luego confirma el resultado con el validador. Los dos se ejecutan enteramente en tu navegador, lo cual importa cuando el archivo que estás arreglando es una configuración de producción con credenciales dentro. Las demás páginas de errores cubren qué hacer cuando el fallo resulta no ser un comentario en absoluto.