Conversor de XML para JSON

Converte para JSON, com cada decisão de mapeamento sob controle.

Entrada
Saída
AguardandoCole um documento para verificá-lo. A validação roda enquanto você digita.

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 &amp; 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": ""
  }
}
Padrões, sobre um documento contendo os quatro casos complicados.

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 &lt; 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".

Ferramentas relacionadas

Leitura complementar