tutoriales·12 min de lectura

XML a JSON: la migración que todo backend legacy necesita hacer bien

Diferencias estructurales XML vs JSON, trampas de la conversión (atributos, namespaces, arrays vs objetos), casos reales de SOAP a REST.

DevToolsHub Team
·
XML a JSON: la migración que todo backend legacy necesita hacer bien

El despertar del desarrollador: cuando SOAP se convierte en tu pesadilla

Llegué a mi trabajo actual hace tres años. El primer día, el CTO me dijo: "Tenemos un sistema SOAP legacy que necesitamos migrar a REST. Es simple, solo convierte XML a JSON". Sonó fácil. Pasé los siguientes seis meses luchando con conversiones que parecían triviales pero escondían trampas que rompían sistemas enteros.

El problema no era la tecnología en sí. XML y JSON son ambos formatos de intercambio de datos. El problema era que estaban diseñados con filosofías completamente diferentes. XML viene del mundo de los documentos (SGML, HTML), JSON viene del mundo de la programación (JavaScript objects). Cuando intentas traducir uno al otro sin entender estas diferencias, creas sistemas frágiles que fallan en los peores momentos posibles.

Este tutorial es lo que desearía haber leído ese primer día. No solo te diré cómo convertir XML a JSON, sino que te explicaré las trampas que he aprendido por las malas, para que no tengas que descubrirlas tú mismo.


XML vs JSON: dos mundos, dos filosofías

Para entender por qué la conversión es difícil, primero necesitas entender qué hace diferente a cada formato.

XML: el mundo de los documentos

XML fue diseñado en los años 90 como una forma de estructurar documentos. Su filosofía es "todo es un elemento con atributos":

<usuario id="123" activo="true">
  <nombre>Juan García</nombre>
  <email>juan@example.com</email>
  <roles>
    <rol>admin</rol>
    <rol>editor</rol>
  </roles>
</usuario>

Características clave de XML:

  • Atributos vs elementos: Los datos pueden estar en atributos (id="123") o en elementos (<nombre>)
  • Namespaces: Los elementos pueden tener prefijos que indican su origen (<soap:Envelope>)
  • Orden significativo: El orden de los elementos puede tener significado semántico
  • Tipos explícitos: A través de schemas (XSD) puedes declarar tipos
  • Comentarios y metadatos: XML soporta comentarios, CDATA, processing instructions

JSON: el mundo de los objetos

JSON nació en 2000 como un subconjunto de JavaScript. Su filosofía es "todo es un objeto o array":

{
  "id": 123,
  "activo": true,
  "nombre": "Juan García",
  "email": "juan@example.com",
  "roles": ["admin", "editor"]
}

Características clave de JSON:

  • Sin atributos: Todo es un par clave-valor
  • Sin namespaces: Solo nombres simples
  • Orden no significativo: Por especificación, el orden de las claves no importa
  • Tipos implícitos: Los tipos se infieren del valor (string, number, boolean, array, object)
  • Sin comentarios: JSON puro no soporta comentarios

Estas diferencias son la raíz de todos los problemas de conversión.


Trampa #1: Atributos vs elementos — ¿dónde van los datos?

Esta es la trampa más común. En XML, los datos pueden estar en atributos o elementos. En JSON, todo es un par clave-valor. ¿Cómo decides?

El problema

<!-- XML original -->
<usuario id="123" activo="true">
  <nombre>Juan García</nombre>
</usuario>

¿Debería convertirse a esto?

// Opción A: atributos como campos normales
{
  "id": 123,
  "activo": true,
  "nombre": "Juan García"
}

¿O a esto?

// Opción B: atributos en un objeto separado
{
  "attributes": {
    "id": 123,
    "activo": true
  },
  "nombre": "Juan García"
}

La solución práctica

En la mayoría de casos, la Opción A es la correcta. Los atributos XML generalmente representan metadatos del elemento (ID, flags), y en JSON esos metadatos son simplemente campos del objeto.

Sin embargo, hay excepciones importantes:

Casos donde usar Opción B (atributos separados):

  1. Cuando hay conflicto de nombres: Si un elemento tiene un atributo y un subelemento con el mismo nombre
<usuario nombre="Juan">
  <nombre>Juan García</nombre>
</usuario>

Aquí necesitas separar para no perder información:

{
  "attributes": {
    "nombre": "Juan"
  },
  "nombre": "Juan García"
}
  1. Cuando necesitas preservar la estructura XML: Si tu sistema downstream necesita saber qué era atributo y qué era elemento

Nuestra herramienta XML a JSON usa la Opción A por defecto, pero te permite configurar el comportamiento según tus necesidades.


Trampa #2: Arrays vs objetos — el problema de la cardinalidad

Esta es la trampa que más sistemas ha roto en producción. En XML, un elemento puede aparecer una vez o múltiples veces. En JSON, tienes que decidir explícitamente si algo es un array o un objeto.

El problema

<!-- XML con un solo rol -->
<usuario>
  <roles>
    <rol>admin</rol>
  </roles>
</usuario>

¿Debería convertirse a esto?

// Opción A: array aunque tenga un elemento
{
  "roles": ["admin"]
}

¿O a esto?

// Opción B: objeto si tiene un elemento
{
  "roles": "admin"
}

Ahora imagina que mañana el usuario tiene dos roles:

<!-- XML con dos roles -->
<usuario>
  <roles>
    <rol>admin</rol>
    <rol>editor</rol>
  </roles>
</usuario>

Si usaste la Opción B, tu conversión ahora falla porque roles ya no es un string, es un array. Tu código downstream que esperaba roles como string ahora rompe.

La solución práctica

Regla de oro: Si un elemento puede aparecer múltiples veces, siempre conviértelo a array, incluso si actualmente solo aparece una vez.

// Siempre array, sin importar el número de elementos
{
  "roles": ["admin"]        // Un elemento → array de uno
}
{
  "roles": ["admin", "editor"]  // Dos elementos → array de dos
}

Esto hace que tu API sea consistente. El cliente siempre puede tratar roles como array, sin importar cuántos roles haya.

Excepción: Si estás 100% seguro de que un elemento nunca aparecerá más de una vez (por ejemplo, <nombre>), puedes usar un string. Pero ten cuidado: las suposiciones de "nunca" suelen romperse.


Trampa #3: Namespaces — el prefijo que no desaparece

XML usa namespaces para evitar conflictos de nombres. JSON no tiene equivalente. ¿Qué haces con los prefijos?

El problema

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <m:GetUser xmlns:m="http://example.com/api">
      <m:id>123</m:id>
    </m:GetUser>
  </soap:Body>
</soap:Envelope>

¿Debería convertirse a esto?

// Opción A: mantener prefijos
{
  "soap:Envelope": {
    "soap:Body": {
      "m:GetUser": {
        "m:id": 123
      }
    }
  }
}

¿O a esto?

// Opción B: eliminar prefijos
{
  "Envelope": {
    "Body": {
      "GetUser": {
        "id": 123
      }
    }
  }
}

La solución práctica

Depende de tu caso de uso:

Para APIs SOAP → REST: Generalmente quieres eliminar los prefijos. Los namespaces SOAP son metadatos de protocolo, no datos de negocio. Tu API REST debería ser limpia:

{
  "user": {
    "id": 123
  }
}

Para conversión genérica: Si estás construyendo una herramienta de conversión genérica (como nuestra herramienta XML a JSON), mantén los prefijos o crea un campo separado para namespaces:

{
  "Envelope": {
    "_xmlns": {
      "soap": "http://schemas.xmlsoap.org/soap/envelope/"
    },
    "Body": {
      "GetUser": {
        "_xmlns": {
          "m": "http://example.com/api"
        },
        "id": 123
      }
    }
  }
}

Esto preserva la información de namespaces sin ensuciar los nombres de los campos.


Trampa #4: Tipos de datos — string vs number vs boolean

XML no tiene tipos nativos. Todo es texto. Los tipos se definen a través de schemas (XSD) o se infieren del contexto. JSON tiene tipos explícitos.

El problema

<usuario>
  <id>123</id>
  <activo>true</activo>
  <saldo>100.50</saldo>
</usuario>

¿Debería convertirse a esto?

// Opción A: todo string (como XML)
{
  "id": "123",
  "activo": "true",
  "saldo": "100.50"
}

¿O a esto?

// Opción B: inferir tipos
{
  "id": 123,
  "activo": true,
  "saldo": 100.50
}

La solución práctica

Inferir tipos siempre que sea posible. JSON con tipos correctos es más útil para los consumidores:

{
  "id": 123,          // number → útil para cálculos
  "activo": true,    // boolean → útil para condiciones
  "saldo": 100.50    // number → útil para cálculos
}

Sin embargo, hay casos donde debes mantener todo como string:

  1. Cuando no tienes un schema XSD: Sin schema, la inferencia es una suposición. 123 podría ser un ZIP code (string) o un ID (number).
  2. Cuando el valor puede tener ceros a la izquierda: 00123 como number se convierte en 123.
  3. Cuando el valor es un número muy grande: JSON tiene limitaciones en la precisión de números grandes.

Nuestra herramienta XML a JSON intenta inferir tipos, pero te permite desactivar esta inferencia si necesitas preservar todo como string.


Trampa #5: Orden de elementos — ¿importa?

En XML, el orden de los elementos puede tener significado. En JSON, por especificación, el orden de las claves de un objeto no debería importar.

El problema

<transaccion>
  <paso1>Validar</paso1>
  <paso2>Procesar</paso2>
  <paso3>Confirmar</paso3>
</transaccion>

Si conviertes a JSON:

{
  "paso1": "Validar",
  "paso2": "Procesar",
  "paso3": "Confirmar"
}

El orden de las claves en el objeto JSON no está garantizado. Un parser podría reordenarlas. Si tu sistema depende del orden, esto rompe.

La solución práctica

Si el orden importa, usa arrays:

{
  "pasos": [
    { "nombre": "paso1", "valor": "Validar" },
    { "nombre": "paso2", "valor": "Procesar" },
    { "nombre": "paso3", "valor": "Confirmar" }
  ]
}

Los arrays en JSON sí preservan orden. Esta es la forma correcta de representar datos secuenciales en JSON.


Implementación práctica: conversión en Node.js

Aquí está un patrón que uso para conversiones robustas:

const xml2js = require('xml2js');

function xmlToJson(xmlString, options = {}) {
  const parser = new xml2js.Parser({
    explicitArray: false,        // No crear arrays para elementos únicos
    mergeAttrs: true,            // Fusionar atributos con elementos
    explicitCharkey: false,      // No usar campo especial para texto
    trim: true,                  // Eliminar espacios en blanco
    ...options
  });

  return new Promise((resolve, reject) => {
    parser.parseString(xmlString, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

// Uso básico
async function convertXML() {
  const xml = `
    <usuario id="123">
      <nombre>Juan</nombre>
      <roles>
        <rol>admin</rol>
        <rol>editor</rol>
      </roles>
    </usuario>
  `;

  const json = await xmlToJson(xml);
  console.log(JSON.stringify(json, null, 2));
}

// Resultado:
// {
//   "usuario": {
//     "id": "123",
//     "nombre": "Juan",
//     "roles": {
//       "rol": ["admin", "editor"]
//     }
//   }
// }

Conversión con inferencia de tipos

function inferTypes(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(inferTypes);
  }

  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    // Intentar convertir a number
    if (typeof value === 'string' && !isNaN(Number(value))) {
      // Verificar que no sea un string que parece número pero no lo es
      if (!value.startsWith('0') || value === '0') {
        result[key] = Number(value);
        continue;
      }
    }

    // Intentar convertir a boolean
    if (value === 'true') {
      result[key] = true;
      continue;
    }
    if (value === 'false') {
      result[key] = false;
      continue;
    }

    // Recursión para objetos anidados
    result[key] = inferTypes(value);
  }

  return result;
}

// Uso
const json = await xmlToJson(xml);
const typedJson = inferTypes(json);

Caso real: migración de SOAP a REST

Aquí está un ejemplo real de una migración que hice. El sistema original tenía este SOAP:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns:GetOrder xmlns:ns="http://example.com/orders">
      <ns:orderId>ORD-12345</ns:orderId>
    </ns:GetOrder>
  </soap:Body>
</soap:Envelope>

Respuesta SOAP:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns:GetOrderResponse xmlns:ns="http://example.com/orders">
      <ns:order id="ORD-12345" status="pending">
        <ns:customer id="CUST-001">
          <ns:name>Juan García</ns:name>
          <ns:email>juan@example.com</ns:email>
        </ns:customer>
        <ns:items>
          <ns:item id="ITEM-001" quantity="2" price="29.99">
            <ns:name>Producto A</ns:name>
          </ns:item>
          <ns:item id="ITEM-002" quantity="1" price="49.99">
            <ns:name>Producto B</ns:name>
          </ns:item>
        </ns:items>
        <ns:total>109.97</ns:total>
      </ns:order>
    </ns:GetOrderResponse>
  </soap:Body>
</soap:Envelope>

API REST equivalente:

// GET /api/orders/ORD-12345
{
  "id": "ORD-12345",
  "status": "pending",
  "customer": {
    "id": "CUST-001",
    "name": "Juan García",
    "email": "juan@example.com"
  },
  "items": [
    {
      "id": "ITEM-001",
      "quantity": 2,
      "price": 29.99,
      "name": "Producto A"
    },
    {
      "id": "ITEM-002",
      "quantity": 1,
      "price": 49.99,
      "name": "Producto B"
    }
  ],
  "total": 109.97
}

Observa las decisiones:

  • Eliminé namespaces (SOAP es metadato de protocolo)
  • Atributos (id, status, quantity, price) se convirtieron a campos normales
  • <items> con múltiples <item> se convirtió a array
  • Tipos inferidos (quantity, price, total como numbers)
  • Estructura simplificada (sin Envelope, Body, Response)

Herramientas recomendadas

Para Node.js

xml2js: La librería más popular. Flexible pero requiere configuración cuidadosa.

npm install xml2js

fast-xml-parser: Más rápida que xml2js, mejor inferencia de tipos.

npm install fast-xml-parser

Para el navegador

Nuestra herramienta XML a JSON funciona completamente en el navegador, sin enviar datos a ningún servidor. Ideal para pruebas rápidas.

Para línea de comandos

# Usando jq (si tienes XML convertido a JSON preliminar)
cat response.xml | xml2json | jq '.'

Checklist para migración exitosa

Antes de migrar tu sistema SOAP/XML a REST/JSON:

☐ Define tu estrategia de arrays

  • ¿Qué elementos siempre serán arrays aunque tengan un solo item?
  • Documenta esta decisión para los consumidores de tu API

☐ Define tu estrategia de atributos

  • ¿Fusionar atributos con elementos o mantenerlos separados?
  • ¿Cómo manejar conflictos de nombres?

☐ Define tu estrategia de namespaces

  • ¿Eliminar prefijos SOAP o preservarlos?
  • ¿Cómo manejar namespaces de negocio?

☐ Define tu estrategia de tipos

  • ¿Inferir tipos o mantener todo como string?
  • ¿Cómo manejar casos límite (números grandes, ceros a la izquierda)?

☐ Prueba con datos reales

  • No solo con XML de ejemplo. Usa datos de producción.
  • Los casos límite aparecen en datos reales, no en ejemplos.

☐ Versiona tu API

  • La primera versión de tu API REST probablemente no será perfecta.
  • Usa versionado (/api/v1/orders) para poder iterar.

Conclusión: la conversión es una traducción, no un transpilado

Convertir XML a JSON no es como transpilar TypeScript a JavaScript. Es más como traducir un poema de español a inglés. No puedes traducir palabra por palabra; tienes que entender el contexto, la cultura, y las intenciones del autor original.

Las trampas que he descrito (atributos, arrays, namespaces, tipos, orden) no son bugs de las herramientas. Son diferencias fundamentales entre dos filosofías de diseño de datos. La mejor herramienta no es la que convierte automáticamente, sino la que te permite configurar la conversión según las necesidades de tu caso específico.

Nuestra herramienta XML a JSON está diseñada con esta filosofía: te da control sobre cada aspecto de la conversión, porque entendemos que cada migración es diferente.

La próxima vez que alguien te diga "solo convierte XML a JSON", ya sabes que no es tan simple. Pero con las estrategias correctas, es completamente manejable.

#xml#json#apis#migración#interoperabilidad

Artículos relacionados