Conversão de XML para JSON: convenções, armadilhas e exemplos de código
Você puxa uma resposta de um endpoint SOAP, de um feed RSS ou de um sitemap.xml, e ela vem em XML. Sua stack é nativa em JSON: JavaScript no front-end, REST no meio, um banco de documentos na base. Então você precisa converter o XML para JSON e recorre a um parser esperando que tudo se resolva em uma linha.
Geralmente é uma linha mesmo, até a saída te morder. Um array que você esperava acaba sendo um único objeto. Um atributo id desaparece. Um CEP como 01234 volta como o número 1234. Nenhum desses casos é um bug do seu parser. Acontecem porque você está mapeando dois modelos de dados que não se alinham, e a única forma de converter XML para JSON de maneira confiável é entender as convenções que fazem a ponte entre eles.
Este guia mostra por que essas convenções existem, quatro maneiras de fazer a conversão (navegador, JavaScript, Python, CLI), as regras de @_ e #text que toda biblioteca importante compartilha, as cinco armadilhas que causam perda silenciosa de dados e como converter JSON de volta para XML para um round-trip limpo. Os exemplos são reais e executáveis: cole-os no Node, no Python ou em um shell e eles produzem exatamente a saída mostrada nos comentários.
Por que XML para JSON precisa de convenções (e não só uma reformatação)
XML e JSON parecem semelhantes à primeira vista, já que ambos são árvores de dados nomeados e aninhados, mas seus modelos subjacentes divergem de maneiras que importam. Elementos XML podem carregar atributos, conter conteúdo misto (texto intercalado com elementos filhos) e viver sob namespaces. JSON não tem nenhum desses conceitos. Ele tem objetos, arrays e quatro tipos escalares. Converter um no outro não é reformatar; é traduzir entre duas gramáticas em que uma tem palavras que a outra não sabe escrever.
Antes de converter qualquer coisa, vale confirmar que a origem é de fato válida. Um & solto sem escape ou uma tag mal fechada serão rejeitados pelo parser, então passar a entrada por um Formatador XML para checar a boa formação primeiro evita uma rodada de erros confusos.
É aqui que os dois modelos se separam:
| Dimensão | XML | JSON |
|---|---|---|
| Tipos de nó | elementos, atributos, texto, conteúdo misto | objetos, arrays, string, number, boolean, null |
| Restrição de raiz | exige exatamente um elemento raiz | sem restrição de raiz |
| Atributos | sim (id="P01") | nenhum (precisa de uma convenção @_) |
| Elementos repetidos | irmãos com o mesmo nome são legais | chaves de objeto não podem repetir (precisa de uma convenção de array) |
| Sistema de tipos | texto não tem tipo — tudo é uma string | tipos nativos |
| Namespaces | sim (xmlns) | nenhum |
Como os modelos não coincidem, toda conversão de XML para JSON é guiada por convenções, não uma reformatação sem perdas. A boa notícia é que as convenções não são arbitrárias: fast-xml-parser (Node.js), xmltodict (Python) e o JAXB (Java) convergiram para os mesmos dois marcadores — @_ para atributos, #text para texto de conteúdo misto. Aprenda-os uma vez e eles se transferem entre runtimes. Esse é o mesmo motivo pelo qual descompassos no formato dos dados aparecem em outras conversões também, como as questões de inferência de tipos no guia de conversão de CSV para JSON.
Como converter XML para JSON: 4 métodos
Escolha o método que se encaixa no seu contexto: uma conversão rápida e pontual colando o texto, um serviço em Node, um pipeline em Python ou um script de shell no CI.
Método 1 — Ferramenta no navegador (zero configuração, privacidade em primeiro lugar)
Para uma conversão pontual, ou para um XML que você prefere não colar em um site qualquer, um conversor no navegador é o caminho mais rápido. Cole o XML no Conversor XML para JSON e o JSON aparece instantaneamente — sem instalação, sem conta, sem upload. Tudo roda no motor JavaScript do seu navegador, então os dados nunca saem da máquina.
Esse último detalhe importa mais do que parece. Envelopes SOAP carregam tokens WS-Security, configs internas carregam strings de conexão, e exportações carregam registros de clientes. Como nada é transmitido, a ferramenta é segura para XML que contenha credenciais ou cargas sensíveis. Você mesmo pode confirmar: abra a aba Network e observe zero requisições serem disparadas enquanto converte.
Método 2 — JavaScript / Node.js (fast-xml-parser)
No Node, fast-xml-parser é a escolha padrão. Os valores default, porém, vão te surpreender: atributos são ignorados e valores sofrem coerção. Por isso as opções abaixo são as que você realmente quer para uma conversão fiel:
// Converte XML para JSON no 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, // mantém os atributos (o default os descarta!)
attributeNamePrefix: '@_', // atributos viram chaves com prefixo @_
textNodeName: '#text', // texto de conteúdo misto vai sob #text
parseAttributeValue: false, // sem coerção de tipo nos atributos
parseTagValue: false, // sem coerção de tipo no texto dos elementos
});
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"
// }
// }
// }
// }
As duas configurações que as pessoas esquecem são ignoreAttributes: false e parseTagValue: false. A primeira mantém seus atributos id e currency; a segunda impede o parser de transformar "79.99" em um float e "01234" em 1234. Voltaremos ao porquê de a preservação como string ser o default seguro na seção de armadilhas.
Se você quer zero dependências no navegador, o DOMParser nativo faz o parsing para você, e você percorre o DOM por conta própria:
// XML para JSON sem dependências no navegador usando DOMParser
function xmlToJson(node) {
// Elemento só com texto → valor string
const children = Array.from(node.children);
if (children.length === 0 && node.attributes.length === 0) {
return node.textContent.trim();
}
const obj = {};
// Atributos → prefixo @_
for (const attr of node.attributes) {
obj['@_' + attr.name] = attr.value;
}
// Elemento com atributos E texto → #text
if (children.length === 0) {
obj['#text'] = node.textContent.trim();
return obj;
}
// Recursão nos filhos, agrupando irmãos de mesmo nome em 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" } } }
O DOMParser é compatível com XML 1.0, trata CDATA e referências de entidade, e relata erros de boa formação, tudo sem instalar pacote. O trade-off é que a lógica de travessia é sua, incluindo a regra de agrupamento em array mostrada acima.
Método 3 — Python (xmltodict)
Em Python, xmltodict reduz o trabalho inteiro a um pipeline curto. Por padrão, ele usa @ como prefixo de atributo e #text para conteúdo misto:
# Converte XML para JSON em 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 padrão, xmltodict mantém todo valor como string, que é o comportamento que você quer. A única opção que vale conhecer desde já é force_list, que corrige o problema de array um-versus-muitos antes que ele chegue ao seu código:
# force_list garante que <product> seja sempre uma lista, mesmo quando há um só
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product'] # agora sempre uma lista
for p in products:
print(p['name'])
Sem force_list, um <product> produz um dict e dois produzem uma lista — e seu loop quebra no caso de item único. Essa é a armadilha nº 1, que cobrimos abaixo.
Método 4 — CLI (yq / one-liner em Python)
Para scripts de shell e pipelines de CI, dois one-liners cobrem a maioria dos casos. O yq de Mike Farah lê XML e emite JSON diretamente:
# Usando yq (a versão em Go de Mike Farah)
yq -p=xml -o=json '.' input.xml
# Lê do stdin via pipe
cat sitemap.xml | yq -p=xml -o=json '.'
Se xmltodict já está no seu ambiente, o one-liner em Python não precisa de nenhum binário extra:
python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml
Ambos leem do stdin em fluxo, então caem direto em um pipeline — útil para converter uma resposta de API no meio de um script ou normalizar um lote de arquivos em uma etapa de build.
As convenções do atributo @_ e do #text explicadas
A maioria das páginas de conversores pula a parte que de fato importa: o que significam as estranhas chaves @_ e #text e por que elas existem. Entenda isso direito e a saída deixa de parecer arbitrária.
Atributos viram chaves com prefixo @_. Um atributo não tem equivalente em JSON — não há um espaço em um objeto para “metadados sobre este objeto” que seja distinto de um filho. A convenção é dar aos atributos uma chave com o prefixo @_:
<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }
Por que @_ especificamente? Porque nenhum nome de elemento XML válido pode começar com @, o prefixo nunca colide com uma chave de elemento filho real. É um namespace reservado escondido à vista de todos. (xmltodict usa apenas @; fast-xml-parser usa @_ por padrão. O princípio é idêntico.)
Conteúdo misto vira #text. Quando um elemento tem tanto um atributo quanto um valor de texto, o texto precisa de algum lugar para viver ao lado das chaves de atributo. Esse lugar é #text:
<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }
Elementos de texto puro viram um valor string direto. Sem atributos, sem filhos, só texto — então não há necessidade da indireção do #text. <name>Alice</name> vira "name": "Alice". A chave #text só aparece quando atributos forçam o valor do elemento a ser um objeto.
Essa assimetria é a origem de um bug sutil. O mesmo nome de elemento pode produzir uma string simples em um documento e um objeto @_/#text em outro, dependendo de aquela instância específica ter ou não carregado um atributo. Um <price> sem atributo currency é a string "29.99"; o mesmo <price currency="USD"> é { "@_currency": "USD", "#text": "29.99" }. Código que lê node.price diretamente funciona para um formato e quebra silenciosamente no outro. O acesso defensivo é verificar o tipo: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.
CDATA vira conteúdo de texto puro. Uma seção <![CDATA[if (a < b) return;]]> é apenas um mecanismo de escape, então os delimitadores são removidos e o texto interno é preservado: "if (a < b) return;". Nada de especial sobrevive até o JSON.
Depois de ter a saída, cole-a em um Formatador JSON para validar o JSON gerado e confirmar que a estrutura corresponde ao que seu consumidor espera antes de conectá-la ao código.
5 armadilhas de XML para JSON e como evitá-las
Estas são as falhas que passam pelo code review e aparecem em produção. Cada uma remete ao descompasso de modelos do início deste guia.
1. Ambiguidade de array (um vs. muitos). Um único <item> vira um objeto; dois ou mais viram um array. O formato do JSON depende de quantos irmãos por acaso estavam naquele documento específico. Código consumidor como result.items.item.forEach(...) funciona nos testes — onde sua fixture tem três itens — e lança TypeError: not a function em produção quando um registro tem exatamente um.
// Dois irmãos <book> → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }
// Um <book> → objeto, NÃO um array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }
// Normaliza para que ambos os casos se comportem de forma idêntica
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // seguro para 0, 1 ou muitos
O idioma [].concat(x ?? []) vale ser memorizado: um valor ausente vira [], um único objeto vira [object], e um array existente passa inalterado. Em Python, passe force_list={'book'} para xmltodict.parse() e o valor é sempre uma lista, então você pula a normalização por completo.
2. Atributos descartados silenciosamente. Várias bibliotecas, por padrão, ignoram atributos — fast-xml-parser faz exatamente isso até você definir ignoreAttributes: false. A conversão parece ter funcionado, o JSON faz parse sem erro, e seus valores id, currency e status simplesmente sumiram. Sempre defina a flag explicitamente em vez de confiar no default.
3. Achatamento de namespaces. Uma declaração xmlns vira uma chave comum @_xmlns, e o prefixo em <soap:Body> sobrevive apenas como parte da chave string "soap:Body". A semântica — que dois prefixos podem se ligar à mesma URI — se perde.
<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": "..."
}
}
O prefixo soap: agora é apenas texto em um nome de chave; nada sabe que ele é um namespace. Se dois elementos de namespaces diferentes compartilham um nome local, eles podem colidir. Quando o tratamento preciso de namespaces faz parte do requisito, mantenha os dados em um parser ciente de namespaces e não os achate em JSON.
4. Sem coerção de tipos, e isso está correto. <zip>01234</zip> não pode virar 1234. Códigos de conta, códigos postais, identificadores com zeros à esquerda e decimais sensíveis à precisão quebram sob coerção silenciosa. Um bom conversor mantém tudo como string e deixa você fazer a coerção deliberadamente:
// Não confie na coerção implícita
if (config.timeout > 25) { /* frágil: "30" > 25 por acaso funciona */ }
// Faça a coerção explicitamente, só onde você conhece o tipo
if (parseInt(config.timeout, 10) > 25) { /* seguro */ }
5. Com perdas: comentários, instruções de processamento e ordem do conteúdo misto. Comentários XML (<!-- ... -->) e instruções de processamento (<?xml-stylesheet ?>) não têm lugar no JSON e são descartados. A ordem relativa de texto intercalado com elementos filhos pode não sobreviver ao round-trip. Se você precisa de cada byte preservado, para reemitir o documento de origem exato, não converta; use um Formatador XML para reformatar ou minificar sem tocar no modelo de dados.
Convertendo JSON de volta para XML (round-trip)
Ir na direção oposta tem sua própria pegadinha, porque JSON não tem regra de elemento raiz e XML exige exatamente um. O Conversor JSON para XML complementar aplica as mesmas convenções @_/#text no sentido inverso, então uma ida JSON → XML → JSON preserva atributos, texto e estrutura.
A parte interessante é a normalização da raiz. O conversor resolve a exigência de raiz única com quatro regras:
- Objeto de chave única → essa chave vira a raiz:
{ "config": {...} }→<config>...</config>. - Objeto de múltiplas chaves → envolvido em
<root>:{ "a": 1, "b": 2 }→<root><a>1</a><b>2</b></root>. - Array no topo → envolvido como
<root><item>...</item></root>, com<item>como nome de fallback fixo. - Valor primitivo →
<root>value</root>.
Todo o resto espelha a direção direta. Chaves @_ viram atributos, #text vira conteúdo de texto, e um array JSON sob uma chave produz irmãos repetidos com o mesmo nome — o nome da chave é reutilizado, nunca colocado no singular:
// Converte JSON para XML no 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: '@_', // chaves @_ viram atributos
textNodeName: '#text', // chave #text vira conteúdo de texto
ignoreAttributes: false, // processa chaves @_
format: true, // pretty-print
});
console.log(builder.build(data));
// <catalog>
// <product id="P01">
// <name>Wireless Headphones</name>
// <price currency="USD">79.99</price>
// </product>
// </catalog>
Um detalhe que o builder trata para você: caracteres especiais em texto e em valores de atributo (<, >, &, ") são escapados para suas referências de entidade, então a saída permanece bem formada.
FAQ
Como os atributos XML são mapeados para JSON?
Atributos viram chaves com o prefixo @_, então id="42" se torna "@_id": "42". Essa é a convenção compartilhada por fast-xml-parser e xmltodict, e o prefixo nunca colide com nomes de elementos porque nenhum nome de elemento válido começa com @.
Por que XML para JSON mantém números como strings?
Porque o conversor não faz coerção de tipos. Forçar 01234 para 1234 descartaria um zero à esquerda significativo de CEPs, números de conta e IDs com zeros de preenchimento. Manter todo valor como string é o default seguro; faça a coerção deliberadamente mais adiante, onde você conhece o tipo.
A conversão de XML para JSON é sem perdas?
Não. Comentários e instruções de processamento são descartados, a semântica de namespaces é apenas parcialmente preservada, e a ordem do conteúdo misto pode não sobreviver ao round-trip. Quando você precisa de cada byte preservado, use um Formatador XML para reformatar o XML em vez de convertê-lo para JSON.
Como os elementos XML repetidos são tratados em JSON?
Um único filho de mesmo nome vira um objeto; dois ou mais viram um array. Como o formato depende da contagem de irmãos, seu código consumidor deve sempre normalizar para um array, de modo que trate tanto o caso de um item quanto o de muitos sem quebrar.
O que acontece com os namespaces XML ao converter para JSON?
Uma declaração xmlns vira uma chave comum @_xmlns, e o prefixo permanece dentro da string do nome do elemento, como em "soap:Body". A ligação semântica de um prefixo a uma URI não é interpretada, então namespaces distintos podem se achatar juntos.
Como converto JSON de volta para XML?
Use o Conversor JSON para XML complementar. Ele aplica as mesmas convenções @_ e #text no sentido inverso, então atributos, conteúdo de texto e arrays mapeiam de volta de forma simétrica. Essa simetria é o que torna possível um round-trip JSON → XML → JSON limpo.
Posso converter XML com múltiplos elementos raiz?
Não. Múltiplos elementos de topo não são XML bem formado, então o parser rejeita a entrada. Envolva os fragmentos em um único elemento raiz primeiro — transforme <a/><b/> em <root><a/><b/></root> — e então converta.
Conclusão
A conversão de XML para JSON é guiada por convenções, não uma reformatação. As regras são consistentes entre runtimes: atributos viram chaves @_, texto de conteúdo misto vira #text, irmãos repetidos viram arrays, e valores permanecem strings para que zeros à esquerda e precisão sobrevivam. As armadilhas a lembrar são a mudança de formato entre um único item e array, atributos descartados silenciosamente, e a perda de comentários e da semântica de namespaces. Nenhuma delas é um bug, e todas são previsíveis assim que você conhece o descompasso de modelos por trás delas.
Quando você precisar de uma conversão rápida e privada, cole no Conversor XML para JSON — ele roda inteiramente no seu navegador. Valide a origem primeiro com o Formatador XML, e siga na direção oposta com o Conversor JSON para XML quando você precisar de XML em round-trip. Para mais sobre como os modelos de formato de dados moldam o comportamento da conversão, veja as notas sobre as diferenças entre YAML e JSON.