Convertidor de XML a JSON
Convierte a JSON, con cada decisión de mapeo en tus manos.
Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.
Pega XML arriba y el JSON aparece mientras escribes. Primero se comprueba la buena formación del documento, porque un conversor que va adivinando entre marcado roto produce un JSON silenciosamente incorrecto en vez de obviamente incorrecto. Si la sintaxis falla obtienes la línea, la columna y la solución en lugar de medio objeto.
Recurres a esto cuando un servicio con el que hablas habla XML y todo lo que hay aguas abajo habla JSON: una respuesta SOAP sobre la que quieres hacer aserciones en un test, un feed de un proveedor que estás metiendo en un script, un archivo ONIX que necesitas hurgar antes de escribir el importador. No se sube nada, lo cual importa cuando el envelope lleva un token de portador.
Lo que aquí es distinto es que el mapeo no está oculto. No hay una única forma correcta de convertir XML en JSON, todos los conversores toman media docena de decisiones por ti, y casi ninguno dice cuáles. Esta página nombra cada decisión, te enseña el interruptor e informa de lo que ha costado la conversión en un panel de notas junto a la salida.
Por qué no hay respuesta correcta, solo una elegida
El modelo de datos de XML es estrictamente más rico que el de JSON. XML tiene hijos ordenados, atributos, texto entremezclado con elementos, espacios de nombres, comentarios y CDATA. JSON tiene objetos sin orden, arrays, cadenas, números, booleanos y null. Cualquier función del primero al segundo debe descartar algo o inventar algo.
Michael Kay lo dijo con claridad en su artículo de Balisage sobre conversión consciente del esquema: un conversor genérico "está adivinando cuál es la semántica del modelo de objetos que hay detrás del XML léxico, y adivina mal". Estos son los siete puntos donde tiene que adivinar:
- Atributos frente a elementos hijos. <user id="7"/> y <user><id>7</id></user> son documentos distintos que casi todo el mundo quiere como el mismo JSON. Fusiónalos y <user id="7"><id>8</id></user> produce una clave duplicada, algo que el RFC 8259 califica de impredecible.
- Una aparición o muchas. Nada en <items><item>a</item></items> dice si item puede repetirse, así que un conversor adivina contando.
- Contenido mixto. <p>Algo de texto <b>es importante</b>.</p> tiene tres hijos ordenados, y un objeto JSON no puede expresar ese orden.
- Texto compuesto solo de espacio en blanco. El salto de línea y la sangría entre elementos en XML formateado son nodos de texto reales, y conservarlos es fiel pero inservible.
- Espacios de nombres. Un prefijo no es el nombre, el nombre es la URI, y JSON no tiene concepto alguno de espacio de nombres.
- Comentarios e instrucciones de procesamiento. Ninguno existe en JSON.
- Elementos vacíos. <e/> mapea de forma plausible a null, "", {} o una clave de texto vacía, y <e/> y <e></e> son el mismo documento.
La convención usada aquí, y los interruptores que la cambian
El valor por defecto es la convención de prefijo de atributo que Stefan Goessner describió en 2006, de la que fast-xml-parser, xml2js y el SDK de AWS usan variantes. Los atributos llevan el prefijo @_ para que no puedan chocar con un elemento hijo del mismo nombre. El texto que comparte elemento con atributos o hijos va bajo #text. Un elemento que aparece una vez es un valor; uno que aparece dos veces se convierte en array. Un elemento de solo texto colapsa a una cadena simple, así que <name>Alice</name> es "Alice".
El elemento raíz se queda como clave más externa, los comentarios se descartan, el espacio en blanco se recorta y las referencias a entidades se resuelven, así que un atributo escrito "A & B" llega como "A & B". Los controles sobre el editor fijan el prefijo de atributo, la clave de texto, una lista de nombres de elemento "siempre un array" y tres casillas: quitar prefijos de espacio de nombres, descartar atributos y convertir tipos. Las tres están desactivadas.
<order id="00042">
<total currency="GBP">19.90</total>
<line sku="0071">Widget</line>
<line sku="0072">Gasket</line>
<note/>
</order>
{
"order": {
"@_id": "00042",
"total": { "@_currency": "GBP", "#text": "19.90" },
"line": [
{ "@_sku": "0071", "#text": "Widget" },
{ "@_sku": "0072", "#text": "Gasket" }
],
"note": ""
}
}Por qué la conversión de tipos está desactivada por defecto
XML sin esquema es texto de arriba abajo. Convertir "123" en un número resulta cómodo justo hasta el momento en que destruye un identificador, y los identificadores son la mayor parte de lo que circula por las integraciones XML.
Cuando sí activas la conversión, es deliberadamente estrecha. Un valor se convierte en número solo si encaja en una gramática estricta de número JSON y luego sobrevive a un viaje de ida y vuelta: el resultado analizado se vuelve a serializar y se compara con el original, carácter a carácter. Esa comprobación es lo que evita los fallos con los que se publican otros conversores. Incluso con la conversión activada, estos siguen siendo cadenas:
- Ceros a la izquierda. "00042" y "01730" no pasan la gramática de entrada, porque un número JSON no puede empezar por un cero seguido de más dígitos. Códigos postales, códigos bancarios y SKU sobreviven.
- Ceros a la derecha tras la coma decimal. "19.90" se analiza como 19.9, que se serializa como "19.9", así que se conserva la cadena. "1.10" no se convierte en 1.1.
- Enteros que un double no puede contener. "9007199254740993" se analiza como un valor que acaba en 992, la ida y vuelta falla, y se conserva la cadena. Esto es lo que corrompe identificadores de pedido de diecinueve dígitos en otros sitios.
- Exponentes no canónicos. "1e5" se analiza como 100000, que no es "1e5", así que se queda como texto.
- Cualquier cosa que no sea exactamente true, false o null. "TRUE", "yes" y "Y" siguen siendo cadenas.
Qué se pierde, y las alternativas con nombre
Tres cosas no sobreviven. El orden del documento entre hermanos con nombres distintos desaparece, así que si <line> y <discount> se alternan, el JSON no dice nada de cómo. El contenido mixto se aplana: los fragmentos de texto se concatenan y los elementos intercalados se mueven a sus propias claves, así que <root>35<nested>34</nested>46</root> da un valor de texto "3546". Los comentarios se descartan.
Los espacios de nombres se conservan literalmente en vez de resolverse, así que soap:Body se convierte en la clave "soap:Body". Quitar los prefijos da "Body", pero entonces dos elementos de espacios de nombres distintos que comparten nombre local chocan en una sola clave, y JSON no ofrece una tercera opción.
Si esta convención no es lo que espera tu consumidor, las alternativas tienen nombre. BadgerFish pone el texto bajo $, los atributos bajo @nombre y arrastra todos los espacios de nombres en ámbito: mejor ida y vuelta, casi ilegible. Parker descarta los atributos y absorbe la raíz, la salida más escueta y más unidireccional de todas. JsonML escribe cada elemento como [nombre, atributos, hijos], la única convención habitual que conserva intactos el contenido mixto y el orden de los hijos.
Hacer esto desde código
La misma conversión en los lenguajes que realmente consumen XML. Cada ejemplo desactiva la resolución de entidades, porque el valor por defecto de varias de estas pilas descargará una URL indicada en un DOCTYPE, que es la vulnerabilidad XXE. Cada uno nombra además la decisión de mapeo que esa biblioteca toma por ti.
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false, // default is true: attributes are DROPPED
attributeNamePrefix: '@_',
textNodeName: '#text',
trimValues: true,
parseTagValue: false, // keep values as strings
parseAttributeValue: false,
processEntities: false, // do not expand DOCTYPE-declared entities
// FXP cannot know whether a tag repeats, so tell it which ones are lists.
isArray: (name) => ['line', 'item', 'entry'].includes(name),
});
const json = parser.parse(xmlSource);
// fast-xml-parser never fetches anything over the network, so XXE is not
// reachable. It does expand entities declared in an internal DTD unless you
// set processEntities: false, so leave that off for untrusted input and cap
// the input size before parsing.import json
import xmltodict
# disable_entities=True is the default in current xmltodict and blocks the
# expat entity-expansion attacks. Pass it explicitly so a downgrade of the
# dependency cannot silently re-enable them.
doc = xmltodict.parse(
xml_source,
disable_entities=True,
attr_prefix='@_',
cdata_key='#text',
force_list=('line', 'item', 'entry'), # the singleton fix
)
print(json.dumps(doc, indent=2, ensure_ascii=False))
# xmltodict returns dicts in document order, but that ordering has no meaning
# once serialised: JSON objects are unordered. Values are always strings.
# There is no coercion, which is the right default.import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import javax.xml.stream.XMLInputFactory;
XMLInputFactory input = XMLInputFactory.newFactory();
// Neither of these is off by default. Both must be, for untrusted XML.
input.setProperty(XMLInputFactory.SUPPORT_DTD, false);
input.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
XmlMapper xml = new XmlMapper(new XmlFactory(input));
JsonNode tree = xml.readTree(xmlSource);
String json = new ObjectMapper()
.writerWithDefaultPrettyPrinter()
.writeValueAsString(tree);
// Jackson's XML module merges attributes in with child elements: there is no
// prefix, so <user id="7"><id>8</id></user> loses one of the two. If your
// documents put data on attributes, bind to a class annotated with
// @JacksonXmlProperty(isAttribute = true) instead of reading a tree.using System.Xml;
using Newtonsoft.Json;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024 * 1024,
MaxCharactersInDocument = 20L * 1024 * 1024,
};
using var reader = XmlReader.Create(new StringReader(xmlSource), settings);
var document = new XmlDocument { XmlResolver = null };
document.Load(reader);
// omitRootObject: false keeps the root element as the outer key.
string json = JsonConvert.SerializeXmlNode(
document, Newtonsoft.Json.Formatting.Indented, omitRootObject: false);
// Json.NET prefixes attributes with "@" and uses "#text" for text, close to
// the convention on this page. It has the singleton problem and no isArray
// hook: the only fix is a json:Array="true" attribute in the source XML,
// which you usually do not control.<?php
libxml_use_internal_errors(true);
// LIBXML_NONET blocks network access for any DTD the document references.
// LIBXML_NOENT is deliberately NOT passed: it would substitute entities.
$xml = simplexml_load_string($source, 'SimpleXMLElement', LIBXML_NONET);
if ($xml === false) {
foreach (libxml_get_errors() as $e) {
fprintf(STDERR, "line %d col %d: %s\n", $e->line, $e->column, trim($e->message));
}
exit(1);
}
echo json_encode($xml, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";
// Two things to know first. json_encode() puts attributes under an
// "@attributes" object, not a prefix. And when an element has both text and
// child elements, SimpleXML drops the text entirely: mixed content does not
// survive this route at all.# yq v4 (Mike Farah) reads XML and writes JSON with no Python dependency.
yq -p=xml -o=json '.' document.xml
# Attributes are prefixed with + by default; match this page's convention:
yq -p=xml -o=json --xml-attribute-prefix='@_' '.' document.xml
# yq resolves nothing over the network. It also has the singleton problem and
# no per-tag array option, so a list of one comes back as a scalar. Normalise
# on the way into jq:
yq -p=xml -o=json '.' document.xml \
| jq '.order.line |= (if type == "array" then . else [.] end)'Todas las bibliotecas de arriba toman al menos una decisión de mapeo en silencio. Jackson fusiona atributos con hijos, SimpleXML descarta el texto del contenido mixto, Json.NET y yq no admiten que les digas qué elementos son listas. Eso es la ambigüedad asomando, no un fallo de ninguna de ellas. Elijas la que elijas, escribe un test que haga pasar una respuesta de un elemento y otra de varios por el mismo camino de código.
Preguntas frecuentes
¿Se envía mi XML a un servidor?
No. El escáner, el mapeador y el serializador JSON son todos JavaScript ejecutándose en esta pestaña, y no hay backend al que la conversión pueda llegar.
Abre tus herramientas de desarrollo, ve a la pestaña Red, pega y observa: los recursos de esta página se cargan una vez y no sigue nada más. Aquí eso importa más que en la mayoría de conversores, porque el XML que la gente convierte suele ser un payload de integración, con su cabecera WS-Security o su clave de API incluidas.
¿Por qué un solo <item> me da un objeto y dos me dan un array?
Porque XML no tiene cardinalidad sin un esquema. Nada en el documento dice si item puede repetirse, así que un conversor adivina contando, lo que hace que la forma de tu JSON dependa de los datos y no del contrato. Esta es la manera más común de romper una integración XML: el código escrito contra una respuesta de prueba con tres elementos llama a items.item.map() y funciona hasta que llega un cliente con un solo pedido.
La solución es el campo "siempre un array" sobre el editor. Haz lo mismo en código: fast-xml-parser tiene isArray, xmltodict tiene force_list, y xml2js pone explicitArray a true por defecto exactamente por esto.
¿Cómo conservo los atributos XML al convertir a JSON?
Se conservan por defecto, bajo claves con el prefijo @_, así que <user id="7"/> se convierte en {"user": {"@_id": "7"}}. El prefijo existe para que un atributo y un elemento hijo con el mismo nombre no puedan pisarse.
Puedes cambiar el prefijo o vaciarlo para fusionar los atributos entre los hijos. La salida fusionada se lee mejor y pierde silenciosamente un valor cuando hay choque de nombres, como en <user id="7"><id>8</id></user>. Para descartar los atributos por completo hay una casilla, que es lo que hace la convención Parker: bien para extracción unidireccional, mal para cualquier cosa que vayas a convertir de vuelta.
¿Qué pasa con los espacios de nombres y prefijos como soap:?
El nombre con prefijo se usa literalmente, así que soap:Body se convierte en la clave "soap:Body". No se resuelve nada, porque JSON no puede llevar una URI de espacio de nombres, y el panel de notas dice cuántos espacios de nombres se declararon.
Marcar "quitar prefijos de espacio de nombres" da "Body", que suele ser lo que quieres al sacar un valor de una respuesta SOAP. El riesgo es real: si el documento tiene tanto soap:Header como wsse:Header, quitarlos los fusiona en una sola clave y gana uno.
¿Debería activar "convertir números y booleanos"?
Solo si sabes que tus datos no contienen identificadores. La conversión de aquí es más estricta que la mayoría, exige una gramática estricta de número y después una comprobación de ida y vuelta, así que "00042", "19.90", "1.10" y cualquier entero demasiado grande para un double siguen siendo cadenas en vez de acabar destrozados.
Lo que no puede cubrir es un campo que vale "1" en tu muestra y "N/A" el martes que viene. Un término medio razonable es dejarlo desactivado y convertir en tu propio código los dos o tres campos que realmente necesitas, donde la decisión queda escrita.
¿Qué hace con los comentarios, CDATA y elementos vacíos?
Los comentarios y las instrucciones de procesamiento se descartan. JSON no tiene dónde ponerlos, e inventar una clave hace la salida más difícil de consumir por un contenido que, por definición, no son datos.
CDATA se trata como texto: <![CDATA[a < b]]> y a < b son el mismo contenido escrito de dos formas. Un elemento vacío pasa a ser la cadena vacía, así que <note/> y <note></note> dan los dos "note": "". Se descartó null porque se lee como "valor desconocido" en vez de "presente y vacío".