Skip to content
Volver al blog
Tutoriales

XML a JSON: convenciones, errores comunes y ejemplos de código

Convierte XML a JSON correctamente: cómo se mapean atributos, arrays y espacios de nombres, por qué los valores siguen siendo cadenas, con código en JavaScript, Python y navegador.

13 min de lectura

Conversión de XML a JSON: convenciones, errores comunes y ejemplos de código

Recibes una respuesta de un endpoint SOAP, un feed RSS o un sitemap.xml, y resulta que es XML. Tu stack es nativo de JSON: JavaScript en el front end, REST en el medio, un almacén de documentos al fondo. Así que necesitas convertir XML a JSON, y echas mano de un parser esperando que sea cosa de una línea.

Y normalmente lo es, hasta que la salida te muerde. Un array que esperabas resulta ser un solo objeto. Un atributo id desaparece. Un código postal como 01234 regresa convertido en el número 1234. Nada de esto son errores de tu parser. Son la consecuencia de mapear dos modelos de datos que no encajan, y la única forma de convertir XML a JSON de manera confiable es entender las convenciones que tienden ese puente.

Esta guía explica por qué existen esas convenciones, cuatro formas de hacer la conversión (navegador, JavaScript, Python, CLI), las reglas de @_ y #text que comparten todas las bibliotecas importantes, los cinco errores comunes que causan pérdida silenciosa de datos, y cómo convertir JSON de vuelta a XML para un viaje de ida y vuelta limpio. Los ejemplos son reales y ejecutables: pégalos en Node, Python o una terminal y producen la salida que aparece en los comentarios.

Por qué la conversión de XML a JSON necesita convenciones (no es solo reformatear)

XML y JSON se parecen en la superficie (ambos son árboles de datos con nombre y anidados), pero sus modelos subyacentes divergen de formas que importan. Los elementos XML pueden llevar atributos, contener contenido mixto (texto intercalado con elementos hijos) y vivir bajo espacios de nombres. JSON no tiene ninguno de esos conceptos. Tiene objetos, arrays y cuatro tipos escalares. Convertir uno en el otro no es reformatear; es traducir entre dos gramáticas donde una tiene palabras que la otra no sabe deletrear.

Antes de convertir nada, conviene confirmar que la fuente es realmente válida. Un & sin escapar o una etiqueta mal cerrada será rechazado por el parser, así que pasar primero la entrada por un Formateador XML para comprobar que esté bien formado te ahorra una ronda de errores confusos.

Aquí es donde los dos modelos se separan:

DimensiónXMLJSON
Tipos de nodoelementos, atributos, texto, contenido mixtoobjetos, arrays, cadena, número, booleano, null
Restricción de raízse requiere exactamente un elemento raízsin restricción de raíz
Atributossí (id="P01")ninguno (necesita una convención @_)
Elementos repetidoshermanos con el mismo nombre son válidoslas claves de objeto no pueden repetirse (necesita una convención de array)
Sistema de tiposel texto no tiene tipo: todo es una cadenatipos nativos
Espacios de nombressí (xmlns)ninguno

Como los modelos no coinciden, toda conversión de XML a JSON está guiada por convenciones, no es un reformateo sin pérdidas. Pero esas convenciones no son arbitrarias: fast-xml-parser (Node.js), xmltodict (Python) y JAXB (Java) convergieron en los mismos dos marcadores: @_ para atributos, #text para el texto de contenido mixto. Apréndelos una vez y se transfieren entre runtimes. Esta es la misma razón por la que los desajustes de forma de datos aparecen también en otras conversiones, como las preguntas de inferencia de tipos en la guía de conversión de CSV a JSON.

Cómo convertir XML a JSON: 4 métodos

Elige el método que se ajuste a tu contexto: un pegado rápido y puntual, un servicio en Node, un pipeline de Python o un script de shell en CI.

Método 1 — Herramienta basada en navegador (cero configuración, privacidad primero)

Para una conversión puntual, o para XML que preferirías no pegar en un sitio web cualquiera, un conversor en el navegador es la vía más rápida. Pega el XML en el Conversor XML a JSON y el JSON aparece al instante: sin instalación, sin cuenta, sin subir nada. Todo se ejecuta en el motor de JavaScript de tu navegador, así que los datos nunca salen de la máquina.

Ese último detalle importa más de lo que parece. Los sobres SOAP llevan tokens WS-Security, las configuraciones internas llevan cadenas de conexión y las exportaciones llevan registros de clientes. Como nada se transmite, la herramienta es segura para XML que contiene credenciales o cargas sensibles. Puedes comprobarlo tú mismo: abre la pestaña Network y observa que se disparan cero peticiones mientras conviertes.

Método 2 — JavaScript / Node.js (fast-xml-parser)

En Node, fast-xml-parser es la opción estándar. Sin embargo, los valores por defecto te sorprenderán: los atributos se ignoran y los valores se convierten de tipo, así que las opciones de abajo son las que realmente quieres para una conversión fiel:

// Convertir XML a JSON en Node.js usando fast-xml-parser
import { XMLParser } from 'fast-xml-parser';

const xml = `<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>`;

const parser = new XMLParser({
  ignoreAttributes: false,    // conservar atributos (¡por defecto los descarta!)
  attributeNamePrefix: '@_',  // los atributos se vuelven claves con prefijo @_
  textNodeName: '#text',      // el texto de contenido mixto va bajo #text
  parseAttributeValue: false, // sin conversión de tipo en atributos
  parseTagValue: false,       // sin conversión de tipo en el texto del elemento
});

const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
//   "catalog": {
//     "product": {
//       "@_id": "P01",
//       "name": "Wireless Headphones",
//       "price": {
//         "@_currency": "USD",
//         "#text": "79.99"
//       }
//     }
//   }
// }

Las dos opciones que la gente olvida son ignoreAttributes: false y parseTagValue: false. La primera conserva tus atributos id y currency; la segunda evita que el parser convierta "79.99" en un float y "01234" en 1234. Volveremos a por qué preservar las cadenas es el valor por defecto seguro en la sección de errores comunes.

Si quieres cero dependencias en el navegador, el DOMParser nativo hace el parseo por ti, y tú recorres el DOM por tu cuenta:

// XML a JSON sin dependencias en el navegador usando DOMParser
function xmlToJson(node) {
  // Elemento solo con texto → valor de cadena
  const children = Array.from(node.children);
  if (children.length === 0 && node.attributes.length === 0) {
    return node.textContent.trim();
  }

  const obj = {};
  // Atributos → prefijo @_
  for (const attr of node.attributes) {
    obj['@_' + attr.name] = attr.value;
  }
  // Elemento con atributos Y texto → #text
  if (children.length === 0) {
    obj['#text'] = node.textContent.trim();
    return obj;
  }
  // Recursión en los hijos, agrupando hermanos con el mismo nombre en arrays
  for (const child of children) {
    const value = xmlToJson(child);
    if (obj[child.tagName] === undefined) {
      obj[child.tagName] = value;
    } else {
      if (!Array.isArray(obj[child.tagName])) obj[child.tagName] = [obj[child.tagName]];
      obj[child.tagName].push(value);
    }
  }
  return obj;
}

const doc = new DOMParser().parseFromString(
  '<catalog><product id="P01"><name>Wireless Headphones</name></product></catalog>',
  'text/xml'
);
const json = { [doc.documentElement.tagName]: xmlToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));
// { "catalog": { "product": { "@_id": "P01", "name": "Wireless Headphones" } } }

DOMParser cumple con XML 1.0, maneja CDATA y referencias de entidades, e informa errores de buena formación, todo sin instalar ningún paquete. La contrapartida es que tú eres dueño de la lógica de recorrido, incluida la regla de agrupación en arrays mostrada arriba.

Método 3 — Python (xmltodict)

En Python, xmltodict reduce todo el trabajo a un pipeline corto. Usa @ como prefijo de atributos y #text para contenido mixto por defecto:

# Convertir XML a JSON en Python usando xmltodict
import json
import xmltodict

xml = """<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>"""

data = xmltodict.parse(xml)
print(json.dumps(data, indent=2))
# {
#   "catalog": {
#     "product": {
#       "@id": "P01",
#       "name": "Wireless Headphones",
#       "price": {
#         "@currency": "USD",
#         "#text": "79.99"
#       }
#     }
#   }
# }

Por defecto xmltodict mantiene cada valor como una cadena, que es el comportamiento que quieres. La única opción que vale la pena conocer de antemano es force_list, que arregla el problema de array de uno-contra-muchos antes de que llegue a tu código:

# force_list garantiza que <product> siempre sea una lista, aun cuando haya uno
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product']  # ahora siempre es una lista
for p in products:
    print(p['name'])

Sin force_list, un solo <product> produce un dict y dos producen una lista, y tu bucle se rompe en el caso de un solo elemento. Ese es el error común n.º 1, que veremos más abajo.

Método 4 — CLI (yq / one-liner de Python)

Para scripts de shell y pipelines de CI, dos one-liners cubren la mayoría de los casos. El yq de Mike Farah lee XML y emite JSON directamente:

# Usando yq (la versión en Go de Mike Farah)
yq -p=xml -o=json '.' input.xml

# Canalizar desde stdin
cat sitemap.xml | yq -p=xml -o=json '.'

Si xmltodict ya está en tu entorno, el one-liner de Python no necesita ningún binario extra:

python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml

Ambos leen en streaming desde stdin, así que se insertan directamente en un pipeline: útil para convertir la respuesta de una API a mitad de un script o normalizar un lote de archivos en un paso de build.

Las convenciones de atributos @_ y #text explicadas

La mayoría de las páginas de conversores se saltan la parte que de verdad importa: qué significan las curiosas claves @_ y #text y por qué existen. Ten esto claro y la salida deja de parecer arbitraria.

Los atributos se mapean a claves con prefijo @_. Un atributo no tiene equivalente en JSON: no hay espacio en un objeto para “metadatos sobre este objeto” distinto de un hijo. La convención es darle a los atributos una clave con prefijo @_:

<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }

¿Por qué @_ en concreto? Porque ningún nombre válido de elemento XML puede empezar con @, el prefijo nunca puede chocar con una clave de elemento hijo real. Es un espacio de nombres reservado escondido a plena vista. (xmltodict usa @ a secas; fast-xml-parser usa @_ por defecto. El principio es idéntico.)

El contenido mixto se mapea a #text. Cuando un elemento tiene a la vez un atributo y un valor de texto, el texto necesita un lugar donde vivir junto a las claves de atributo. Ese lugar es #text:

<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }

Los elementos de texto plano se vuelven un valor de cadena directo. Sin atributos, sin hijos, solo texto, así que no hace falta la indirección de #text. <name>Alice</name> se vuelve "name": "Alice". La clave #text solo aparece cuando los atributos obligan a que el valor del elemento sea un objeto.

Esta asimetría es la fuente de un error sutil. El mismo nombre de elemento puede producir una cadena plana en un documento y un objeto @_/#text en otro, según si esa instancia particular llevaba un atributo. Un <price> sin atributo currency es la cadena "29.99"; el mismo <price currency="USD"> es { "@_currency": "USD", "#text": "29.99" }. El código que lee node.price directamente funciona para una forma y se rompe en silencio con la otra. El accesor defensivo consiste en comprobar el tipo: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.

CDATA se vuelve contenido de texto plano. Una sección <![CDATA[if (a < b) return;]]> es solo un mecanismo de escape, así que se eliminan los delimitadores y se preserva el texto interior: "if (a < b) return;". Nada especial sobrevive al pasar al JSON.

Una vez que tengas la salida, pégala en un Formateador JSON para validar el JSON resultante y confirmar que la estructura coincide con lo que tu consumidor espera antes de conectarlo al código.

5 errores comunes de XML a JSON y cómo evitarlos

Estos son los fallos que pasan la revisión de código y aparecen en producción. Cada uno se remonta al desajuste de modelos del inicio de esta guía.

1. Ambigüedad de array (uno contra muchos). Un solo <item> se vuelve un objeto; dos o más se vuelven un array. La forma del JSON depende de cuántos hermanos hubiera por casualidad en ese documento en concreto. Código de consumo como result.items.item.forEach(...) funciona en las pruebas (donde tu fixture tiene tres elementos) y lanza TypeError: not a function en producción cuando un registro tiene exactamente uno.

// Dos hermanos <book> → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }

// Un solo <book> → objeto, NO un array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }

// Normaliza para que ambos casos se comporten igual
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // seguro para 0, 1 o muchos

Vale la pena memorizar el modismo [].concat(x ?? []): un valor ausente se vuelve [], un solo objeto se vuelve [object] y un array existente pasa sin cambios. En Python, pasa force_list={'book'} a xmltodict.parse() y el valor siempre será una lista, así que te saltas la normalización por completo.

2. Atributos descartados en silencio. Varias bibliotecas ignoran los atributos por defecto: fast-xml-parser hace exactamente esto hasta que defines ignoreAttributes: false. La conversión parece haber funcionado, el JSON se parsea bien, y tus valores id, currency y status simplemente desaparecieron. Define siempre el flag de forma explícita en lugar de confiar en el valor por defecto.

3. Aplanamiento de espacios de nombres. Una declaración xmlns se vuelve una clave @_xmlns corriente, y el prefijo en <soap:Body> sobrevive solo como parte de la clave de cadena "soap:Body". La semántica (que dos prefijos puedan vincularse al mismo URI) se pierde.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>...</soap:Body>
</soap:Envelope>
→ {
    "soap:Envelope": {
      "@_xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
      "soap:Body": "..."
    }
  }

El prefijo soap: ahora es solo texto en un nombre de clave; nada sabe que es un espacio de nombres. Si dos elementos de espacios de nombres distintos comparten un nombre local, pueden chocar. Cuando el manejo preciso de espacios de nombres forma parte del requisito, mantén los datos en un parser que reconozca espacios de nombres y no los aplanes a JSON en absoluto.

4. Sin conversión de tipos, y eso es correcto. <zip>01234</zip> no debe volverse 1234. Los códigos de cuenta, los códigos postales, los identificadores con relleno y los decimales sensibles a la precisión se rompen todos bajo conversión silenciosa. Un buen conversor mantiene todo como cadena y te deja convertir de forma deliberada:

// No confíes en la conversión implícita
if (config.timeout > 25) { /* frágil: "30" > 25 funciona por casualidad */ }

// Convierte de forma explícita, solo donde conoces el tipo
if (parseInt(config.timeout, 10) > 25) { /* seguro */ }

5. Con pérdidas: comentarios, instrucciones de procesamiento y orden del contenido mixto. Los comentarios XML (<!-- ... -->) y las instrucciones de procesamiento (<?xml-stylesheet ?>) no tienen hogar en JSON y se descartan. El orden relativo del texto intercalado con elementos hijos puede no sobrevivir al viaje de ida y vuelta. Si necesitas preservar cada byte (para reemitir el documento de origen exacto), no conviertas en absoluto; usa un Formateador XML para reformatear o minificar sin tocar el modelo de datos.

Convertir JSON de vuelta a XML (viaje de ida y vuelta)

Ir en la dirección contraria tiene su propio giro, porque JSON no tiene regla de elemento raíz y XML requiere exactamente uno. El Conversor JSON a XML complementario aplica las mismas convenciones @_/#text a la inversa, así que un viaje JSON → XML → JSON preserva atributos, texto y estructura.

La parte interesante es la normalización de la raíz. El conversor resuelve el requisito de raíz única con cuatro reglas:

  • Objeto de una sola clave → esa clave se vuelve la raíz: { "config": {...} }<config>...</config>.
  • Objeto de varias claves → envuelto en <root>: { "a": 1, "b": 2 }<root><a>1</a><b>2</b></root>.
  • Array de nivel superior → envuelto como <root><item>...</item></root>, con <item> como nombre de respaldo fijo.
  • Valor primitivo<root>value</root>.

Todo lo demás refleja la dirección de ida. Las claves @_ se vuelven atributos, #text se vuelve contenido de texto, y un array JSON bajo una clave produce hermanos repetidos con el mismo nombre: el nombre de la clave se reutiliza, nunca se singulariza:

// Convertir JSON a XML en Node.js usando fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';

const data = {
  catalog: {
    product: {
      '@_id': 'P01',
      name: 'Wireless Headphones',
      price: { '@_currency': 'USD', '#text': '79.99' },
    },
  },
};

const builder = new XMLBuilder({
  attributeNamePrefix: '@_', // las claves @_ se vuelven atributos
  textNodeName: '#text',     // la clave #text se vuelve contenido de texto
  ignoreAttributes: false,   // procesar las claves @_
  format: true,              // impresión legible
});

console.log(builder.build(data));
// <catalog>
//   <product id="P01">
//     <name>Wireless Headphones</name>
//     <price currency="USD">79.99</price>
//   </product>
// </catalog>

Un detalle que el builder maneja por ti: los caracteres especiales en el texto y en los valores de atributo (<, >, &, ") se escapan a sus referencias de entidad, así que la salida sigue estando bien formada.

Preguntas frecuentes

¿Cómo se mapean los atributos XML a JSON?

Los atributos se vuelven claves con prefijo @_, así que id="42" se convierte en "@_id": "42". Esta es la convención compartida de fast-xml-parser y xmltodict, y el prefijo nunca choca con los nombres de elementos porque ningún nombre de elemento válido empieza con @.

¿Por qué la conversión de XML a JSON mantiene los números como cadenas?

Porque el conversor no hace conversión de tipos. Forzar 01234 a 1234 quitaría un cero inicial significativo de códigos postales, números de cuenta e IDs con relleno. Mantener cada valor como cadena es el valor por defecto seguro; convierte de forma deliberada más adelante donde conozcas el tipo.

¿La conversión de XML a JSON es sin pérdidas?

No. Los comentarios y las instrucciones de procesamiento se descartan, la semántica de los espacios de nombres se preserva solo parcialmente, y el orden del contenido mixto puede no sobrevivir al viaje de ida y vuelta. Cuando necesites preservar cada byte, usa un Formateador XML para reformatear el XML en vez de convertirlo a JSON.

¿Cómo se manejan los elementos XML repetidos en JSON?

Un solo hijo con el mismo nombre se vuelve un objeto; dos o más se vuelven un array. Como la forma depende del número de hermanos, tu código de consumo debería normalizar siempre a un array para manejar tanto el caso de un elemento como el de muchos sin romperse.

¿Qué les pasa a los espacios de nombres XML al convertir a JSON?

Una declaración xmlns se vuelve una clave @_xmlns corriente, y el prefijo se queda dentro de la cadena del nombre del elemento, como en "soap:Body". El vínculo semántico de un prefijo a un URI no se interpreta, así que espacios de nombres distintos pueden aplanarse juntos.

¿Cómo convierto JSON de vuelta a XML?

Usa el Conversor JSON a XML complementario. Aplica las mismas convenciones @_ y #text a la inversa, así que los atributos, el contenido de texto y los arrays se mapean de vuelta de forma simétrica. Esa simetría es lo que hace posible un viaje JSON → XML → JSON limpio.

¿Puedo convertir XML con varios elementos raíz?

No. Varios elementos de nivel superior no son XML bien formado, así que el parser rechaza la entrada. Envuelve primero los fragmentos en un solo elemento raíz (convierte <a/><b/> en <root><a/><b/></root>) y luego conviértelo.

Conclusión

La conversión de XML a JSON está guiada por convenciones, no es un reformateo. Las reglas son consistentes entre runtimes: los atributos se mapean a claves @_, el texto de contenido mixto a #text, los hermanos repetidos a arrays, y los valores siguen siendo cadenas para que los ceros iniciales y la precisión sobrevivan. Las trampas que hay que recordar son el cambio de forma de uno-contra-array, los atributos descartados en silencio y la pérdida de comentarios y de semántica de espacios de nombres: ninguna de ellas son errores, todas son predecibles una vez que conoces el desajuste de modelos que hay detrás.

Cuando necesites una conversión rápida y privada, pega en el Conversor XML a JSON: se ejecuta por completo en tu navegador. Valida primero la fuente con el Formateador XML, y ve en la dirección contraria con el Conversor JSON a XML cuando necesites XML de ida y vuelta. Para más sobre cómo los modelos de formato de datos moldean el comportamiento de conversión, consulta las notas sobre las diferencias entre YAML y JSON.

Artículos relacionados

Ver todos los artículos