Conversor de XML para JSON
Converte para JSON, com cada decisão de mapeamento sob controle.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole XML acima e o JSON aparece enquanto você digita. O documento é primeiro verificado quanto à boa formação, porque um conversor que vai adivinhando por dentro de marcação quebrada produz um JSON silenciosamente errado em vez de obviamente errado. Se a sintaxe falhar, você recebe a linha, a coluna e a correção em vez de meio objeto.
Você recorre a isto quando um serviço com quem você fala usa XML e tudo abaixo de você usa JSON: uma resposta SOAP sobre a qual você quer fazer asserções num teste, um feed de fornecedor que está puxando para um script, um arquivo ONIX que precisa cutucar antes de escrever o importador. Nada é enviado, o que importa quando o envelope carrega um token.
O que é diferente aqui é que o mapeamento não fica escondido. Não existe uma única forma correta de transformar XML em JSON; todo conversor toma meia dúzia de decisões por você, e quase nenhum diz quais. Esta página nomeia cada decisão, mostra o botão correspondente e informa o que a conversão custou num painel de notas ao lado da saída.
Por que não há resposta certa, só uma escolhida
O modelo de dados do XML é estritamente mais rico que o do JSON. XML tem filhos ordenados, atributos, texto intercalado com elementos, namespaces, comentários e CDATA. JSON tem objetos sem ordem, arrays, strings, números, booleanos e null. Qualquer função do primeiro para o segundo precisa descartar algo ou inventar algo.
Michael Kay disse isso sem rodeios no artigo dele no Balisage sobre conversão ciente de esquema: um conversor genérico "está adivinhando qual é a semântica do modelo de objetos por trás do XML léxico, e está adivinhando errado". Estes são os sete pontos em que ele precisa adivinhar:
- Atributos versus elementos filhos. <user id="7"/> e <user><id>7</id></user> são documentos diferentes que a maioria das pessoas quer como o mesmo JSON. Junte-os e <user id="7"><id>8</id></user> produz uma chave duplicada, o que a RFC 8259 chama de imprevisível.
- Uma ocorrência ou várias. Nada em <items><item>a</item></items> diz se item pode se repetir, então o conversor adivinha contando.
- Conteúdo misto. <p>Um texto <b>importante</b>.</p> tem três filhos ordenados, e um objeto JSON não consegue expressar essa ordem.
- Texto só de espaço em branco. A quebra de linha e a indentação entre elementos em XML formatado são nós de texto reais, e mantê-los é fiel mas inutilizável.
- Namespaces. Um prefixo não é o nome, o nome é a URI, e o JSON não tem nenhum conceito de namespace.
- Comentários e instruções de processamento. Nenhum dos dois existe em JSON.
- Elementos vazios. <e/> mapeia de forma plausível para null, "", {} ou uma chave de texto vazia, e <e/> e <e></e> são o mesmo documento.
A convenção usada aqui, e os controles que a mudam
O padrão é a convenção de prefixo de atributo que Stefan Goessner descreveu em 2006, da qual fast-xml-parser, xml2js e o SDK da AWS usam variantes. Atributos recebem o prefixo @_ para não colidirem com um elemento filho de mesmo nome. Texto que divide um elemento com atributos ou filhos vai para #text. Um elemento que aparece uma vez é um valor; aparecendo duas vezes vira array. Um elemento só de texto colapsa para uma string simples, então <name>Alice</name> é "Alice".
O elemento raiz permanece como chave mais externa, comentários são descartados, espaço em branco é aparado e referências de entidade são resolvidas, então um atributo escrito "A & B" chega como "A & B". Os controles acima do editor definem o prefixo de atributo, a chave de texto, uma lista de nomes de elemento "sempre um array", e três caixas: remover prefixos de namespace, descartar atributos e converter tipos. As três vêm desligadas.
<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 que a conversão de tipos vem desligada
XML sem esquema é texto do começo ao fim. Transformar "123" em número é conveniente até exatamente o ponto em que destrói um identificador, e identificadores são a maior parte do que trafega em integrações XML.
Quando você liga a conversão, ela é deliberadamente estreita. Um valor só vira número se casar com uma gramática estrita de número JSON e depois sobreviver a uma ida e volta: o resultado analisado é serializado de novo e comparado com o original, caractere por caractere. É essa checagem que evita as falhas que outros conversores entregam. Mesmo com a conversão ligada, estes continuam strings:
- Zeros à esquerda. "00042" e "01730" falham logo na gramática, porque um número JSON não pode começar com zero seguido de mais dígitos. CEPs, códigos bancários e SKUs sobrevivem.
- Zeros à direita depois da vírgula decimal. "19.90" vira 19.9, que serializa de volta como "19.9", então a string é mantida. "1.10" não vira 1.1.
- Inteiros que um double não comporta. "9007199254740993" vira um valor terminado em 992, a ida e volta falha, e a string é mantida. É isso que corrompe identificadores de pedido de dezenove dígitos em outros lugares.
- Expoentes não canônicos. "1e5" vira 100000, que não é "1e5", então continua texto.
- Qualquer coisa que não seja exatamente true, false ou null. "TRUE", "yes" e "Y" continuam strings.
O que se perde, e as alternativas com nome
Três coisas não sobrevivem. A ordem do documento entre irmãos de nomes diferentes some, então se <line> e <discount> se alternam, o JSON não diz nada sobre como. Conteúdo misto é achatado: os fragmentos de texto são concatenados e os elementos entre eles vão para chaves próprias, então <root>35<nested>34</nested>46</root> dá um valor de texto "3546". Comentários são descartados.
Namespaces são mantidos literalmente em vez de resolvidos, então soap:Body vira a chave "soap:Body". Remover prefixos dá "Body", mas aí dois elementos de namespaces diferentes com o mesmo nome local colidem numa chave só, e o JSON não oferece uma terceira opção.
Se esta convenção não é o que o seu consumidor espera, as alternativas têm nome. O BadgerFish põe texto em $, atributos em @nome e carrega todo namespace em escopo: melhor ida e volta, quase ilegível. O Parker descarta atributos e absorve a raiz, a saída mais enxuta e mais de mão única de todas. O JsonML escreve cada elemento como [nome, atributos, filhos], a única convenção comum que preserva intactos o conteúdo misto e a ordem dos filhos.
Fazendo isso em código
A mesma conversão nas linguagens que realmente consomem XML. Cada exemplo desativa a resolução de entidades, porque o padrão de várias dessas pilhas busca uma URL indicada num DOCTYPE, que é a vulnerabilidade XXE. Cada um também nomeia a decisão de mapeamento que aquela biblioteca toma por você.
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)'Toda biblioteca acima toma ao menos uma decisão de mapeamento em silêncio. O Jackson funde atributos com filhos, o SimpleXML descarta o texto do conteúdo misto, Json.NET e yq não têm como saber quais elementos são listas. Isso é a ambiguidade aparecendo, não um bug de nenhuma delas. Qualquer que você escolha, escreva um teste que passe uma resposta de um item e outra de vários pelo mesmo caminho de código.
Perguntas frequentes
Meu XML é enviado para um servidor?
Não. O scanner, o mapeador e o serializador JSON são todos JavaScript rodando nesta aba, e não existe backend que a conversão possa alcançar.
Abra as ferramentas de desenvolvedor, vá para a aba Rede, cole e observe: os recursos desta página carregam uma vez e nada mais acontece. Isso importa mais aqui do que na maioria dos conversores, porque o XML que as pessoas convertem costuma ser um payload de integração, com cabeçalho WS-Security ou chave de API junto.
Por que um <item> me dá um objeto e dois me dão um array?
Porque XML não tem cardinalidade sem um esquema. Nada no documento diz se item pode se repetir, então o conversor adivinha contando, o que faz o formato do seu JSON depender dos dados em vez do contrato. Essa é a forma mais comum de uma integração XML quebrar: código escrito contra uma resposta de teste com três itens chama items.item.map() e funciona até chegar um cliente com um pedido só.
A saída é o campo "sempre um array" acima do editor. Faça o mesmo no código: o fast-xml-parser tem isArray, o xmltodict tem force_list, e o xml2js deixa explicitArray como true por padrão exatamente por isso.
Como mantenho os atributos XML ao converter para JSON?
Eles são mantidos por padrão, em chaves com o prefixo @_, então <user id="7"/> vira {"user": {"@_id": "7"}}. O prefixo existe para que um atributo e um elemento filho de mesmo nome não se sobrescrevam.
Você pode mudar o prefixo ou esvaziá-lo para misturar os atributos entre os filhos. A saída misturada lê melhor e perde silenciosamente um valor quando há colisão de nomes, como em <user id="7"><id>8</id></user>. Para descartar atributos de vez existe uma caixa, que é o que a convenção Parker faz: ótima para extração de mão única, errada para qualquer coisa que você vá converter de volta.
O que acontece com namespaces e prefixos como soap:?
O nome com prefixo é usado literalmente, então soap:Body vira a chave "soap:Body". Nada é resolvido, porque o JSON não consegue carregar uma URI de namespace, e o painel de notas informa quantos namespaces foram declarados.
Marcar "remover prefixos de namespace" dá "Body", geralmente o que você quer ao tirar um valor de uma resposta SOAP. O risco é real: se o documento tem soap:Header e wsse:Header, remover funde os dois numa chave só e um deles vence.
Devo ligar "converter números e booleanos"?
Só se você souber que seus dados não têm identificadores. A conversão aqui é mais rígida que a maioria, exigindo gramática estrita de número e depois uma checagem de ida e volta, então "00042", "19.90", "1.10" e qualquer inteiro grande demais para um double continuam strings em vez de serem estragados.
O que ela não cobre é um campo que vale "1" na sua amostra e "N/A" na terça que vem. Um meio-termo razoável é deixar desligado e converter no seu próprio código os dois ou três campos de que você realmente precisa, onde a decisão fica registrada.
O que ele faz com comentários, CDATA e elementos vazios?
Comentários e instruções de processamento são descartados. O JSON não tem onde colocá-los, e inventar uma chave só deixa a saída mais difícil de consumir por causa de um conteúdo que, por definição, não é dado.
CDATA é tratado como texto: <![CDATA[a < b]]> e a < b são o mesmo conteúdo escrito de dois jeitos. Um elemento vazio vira string vazia, então <note/> e <note></note> dão ambos "note": "". null foi rejeitado porque se lê como "valor desconhecido" em vez de "presente e vazio".