Cómo construir un formateador de JSON en JavaScript puro

JSON.parse y JSON.stringify hacen el trabajo pesado. Lo interesante está en los detalles: extraer la posición del error, calcular línea y columna, y sincronizar dos áreas de texto.

Formateador de JSON online: texto JSON sin formato a la izquierda, versión indentada a la derecha

Un formateador de JSON suena trivial: parseas, stringificas con sangría, listo. Y en su forma más básica lo es. Pero en cuanto intentas dar feedback útil al usuario —dónde falla exactamente, cuántos niveles tiene, qué tipo de dato es la raíz— hay suficiente chicha para que valga la pena documentarlo.

El núcleo: parse y stringify

JavaScript tiene todo lo necesario de serie. Para validar y formatear:

try {
  var parsed = JSON.parse(input);
  var formateado = JSON.stringify(parsed, null, 2);
} catch (err) {
  // error de sintaxis
}

El segundo argumento de JSON.stringify es un replacer (aquí null, sin filtrado). El tercero es la sangría: 2 da dos espacios por nivel, 4 da cuatro, '\t' usa tabuladores.

Para minificar es lo mismo sin el tercer argumento:

var minificado = JSON.stringify(parsed);

Extraer línea y columna del error

Cuando JSON.parse lanza un SyntaxError, el mensaje incluye la posición del carácter problemático: Unexpected token } at position 42. Esa posición es un offset de carácter desde el inicio del string.

Convertirla a línea y columna es aritmética de strings:

function parsearError(err, input) {
  var msg      = err.message || '';
  var posMatch = msg.match(/position (\d+)/i);
  if (!posMatch) return msg;

  var pos   = parseInt(posMatch[1], 10);
  var antes = input.slice(0, pos);
  var linea = (antes.match(/\n/g) || []).length + 1;
  var col   = pos - antes.lastIndexOf('\n');

  return 'Error en línea ' + linea + ', columna ' + col;
}

antes.lastIndexOf('\n') devuelve -1 si no hay saltos de línea, lo que hace que col sea pos + 1 —exactamente lo correcto para la primera línea.

Detectar el tipo raíz y contar claves

Un JSON válido puede ser cualquier valor: objeto, array, string, número, booleano o null. Mostrar el tipo al usuario ayuda a confirmar que ha pegado lo correcto:

var tipo = Array.isArray(parsed) ? 'Array'
         : parsed === null       ? 'null'
         : typeof parsed;

Para contar claves en objetos anidados, una función recursiva:

function contarClaves(obj) {
  if (obj === null || typeof obj !== 'object') return 0;
  return Object.keys(obj).length
    + Object.values(obj).reduce(function(acc, v) {
        return acc + contarClaves(v);
      }, 0);
}

Y la profundidad máxima de anidamiento, que da una idea de la complejidad:

function profundidadMax(obj, nivel) {
  nivel = nivel || 0;
  if (obj === null || typeof obj !== 'object') return nivel;
  var hijos = Object.values(obj).map(function(v) {
    return profundidadMax(v, nivel + 1);
  });
  return hijos.length ? Math.max.apply(null, hijos) : nivel;
}

Copiar al portapapeles

La API moderna es navigator.clipboard.writeText(), que devuelve una promesa. Merece un fallback al método antiguo porque algunos contextos (HTTP sin localhost, iframes de ciertos orígenes) la tienen restringida:

function copiar(texto) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(texto);
    return;
  }
  // fallback para contextos sin permisos de clipboard
  var tmp = document.createElement('textarea');
  tmp.value = texto;
  tmp.style.position = 'fixed';
  tmp.style.opacity  = '0';
  document.body.appendChild(tmp);
  tmp.select();
  document.execCommand('copy');
  document.body.removeChild(tmp);
}

Sincronizar entrada y salida

El patrón es un único listener en el textarea de entrada que actualiza todo lo demás: el estado de validación, el textarea de salida y el estado habilitado/deshabilitado de los botones de copia. Una sola fuente de verdad:

inputEl.addEventListener('input', function () {
  var raw = inputEl.value;
  if (!raw.trim()) { resetear(); return; }

  try {
    var parsed = JSON.parse(raw);
    outputEl.value = JSON.stringify(parsed, null, 2);
    mostrarExito(parsed);
    habilitarBotones(true);
  } catch (err) {
    outputEl.value = '';
    mostrarError(err, raw);
    habilitarBotones(false);
  }
});

No hace falta debounce: JSON.parse de JSONs razonablemente grandes (cientos de KB) es instantáneo en cualquier motor moderno.

A continuación puedes probar la herramienta completa.

Abrir en página propia

Herramienta · paigar.eu

Formateador de JSON

Pega tu JSON para validarlo al instante. Obtendrás la versión formateada con sangría y los errores de sintaxis indicados con precisión.

Esperando JSON…
···
Otras entradas