Conversor de JSON para XML

Converte para XML e mostra o que foi renomeado e por quê.

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 JSON acima e um XML bem formado aparece ao lado, com declaração, indentação de verdade e todo caractere especial escapado. Se o JSON não fizer parsing você recebe a mensagem do próprio analisador em vez de um painel em branco, e se alguma chave precisou ser renomeada para virar um nome de elemento XML legal, a ferramenta diz qual e no que ela virou.

Você precisa disto quando alguma coisa lá adiante só fala XML: um endpoint SOAP, a importação de um ERP legado, um formato de troca validado por XSD, uma fixture para um serviço que você está simulando. É a metade menos glamourosa do par e a de arestas mais afiadas, porque o XML impõe restrições que o JSON não tem e essas restrições precisam ser resolvidas em algum lugar.

Tudo roda nesta aba. Nada é enviado, e vale dizer isso porque o JSON que as pessoas colam em conversores costuma ser uma resposta de API capturada, e respostas de API capturadas contêm tokens, números de conta e registros de clientes.

XML precisa de exatamente uma raiz. JSON não.

A RFC 8259 permite qualquer valor no nível superior de um documento JSON: um objeto, um array, uma string, um número, true, false ou null. O XML 1.0 exige exatamente um elemento raiz contendo todo o resto, então esse descompasso é resolvido em toda conversão.

Um objeto com exatamente uma chave já tem raiz natural, então essa chave vira o elemento raiz e nada é inventado. {"order": {...}} dá <order>...</order> sem invólucro, que é o caso comum porque é o formato que a conversão de XML para JSON devolve. Qualquer outra coisa é envolvida, e o painel de notas avisa:

  • Um objeto com duas ou mais chaves de nível superior é envolvido num único elemento, chamado root por padrão e editável na linha de controles.
  • Um array de nível superior é envolvido duas vezes, porque um array não tem nome de elemento próprio: cada membro vira <item> dentro de <root>.
  • Um escalar de nível superior vira o texto do elemento raiz, então o documento JSON 42 dá <root>42</root>.
  • Um null de nível superior dá um elemento raiz vazio, <root/>.

Arrays repetem o nome do elemento. Eles não ganham invólucro.

Esta é a decisão que a maioria dos conversores inverte. {"line": ["a", "b"]} vira dois elementos <line> irmãos, não um elemento <line> com dois filhos <item>. Repetição é como o XML expressa uma lista; é a própria razão de a direção XML para JSON ter o problema do singleton. Inventar um invólucro produz XML que nenhum esquema existente aceitaria, e que não faria a volta.

Duas consequências vêm daí. Um array vazio não produz nada, então a chave some: zero repetições de um elemento são zero elementos. E um array de arrays achata, porque o array interno não tem nome distinto do externo, então [[1,2],[3]] sob a chave a dá três elementos <a>. Se algum dos dois importa, reestruture o JSON antes.

{
  "order": {
    "@_id": "00042",
    "line": [ "Widget", "Gasket" ],
    "note": null,
    "meta": {},
    "tags": []
  }
}

<?xml version="1.0" encoding="UTF-8"?>
<order id="00042">
  <line>Widget</line>
  <line>Gasket</line>
  <note/>
  <meta/>
</order>
Arrays, null, objetos vazios e arrays vazios.

Chaves JSON frequentemente não são nomes XML legais

A seção 2.3 do XML 1.0 define um Name como um NameStartChar seguido de NameChars. Um NameStartChar é uma letra, um sublinhado ou dois-pontos; não é um dígito, um espaço, um "e comercial" nem um cifrão. Uma chave JSON não tem essa restrição, então "2024 total", "user@email" e "$ref" são chaves comuns e nenhuma delas é um nome de elemento legal.

O mundo .NET e XSD escapa, transformando um espaço em _x0020_, o que é exato e ilegível. Esta ferramenta sanitiza e reporta no lugar: caracteres ilegais são substituídos um a um em vez de removidos, e um nome que ainda comece com dígito ganha um prefixo. É esse o ponto, porque remover junta chaves distintas num mesmo nome e substituir não. Toda renomeação aparece no painel de notas.

  • "2024 total" vira _2024_total: o espaço é substituído, e então o dígito inicial força o prefixo.
  • "2024-total" vira _2024-total: o hífen já é legal, então só o dígito inicial precisa do prefixo. As duas continuam distintas, que é exatamente o que remover teria custado.
  • "user@email" vira user_email, "$ref" vira _ref, e uma chave vazia vira um sublinhado sozinho.
  • Uma chave que já é legal passa intacta, inclusive uma com dois-pontos: "soap:Body" continua "soap:Body". Isso dá um elemento com prefixo e sem declaração xmlns, que é bem formado mas não é correto quanto a namespaces.

Atributos, texto, e o que o JSON perde primeiro

Chaves que começam com @_ viram atributos do elemento que as envolve, e uma chave chamada #text fornece o conteúdo textual. As duas combinam com a direção XML para JSON, então a saída daquela página converte de volta direto. Valores de atributo são escapados com mais rigor que o texto: além de &, < e ", o writer escapa tabulação, quebra de linha e retorno de carro como referências numéricas, porque a seção 3.3.3 do XML 1.0 normaliza para espaços o branco literal em valores de atributo ao reanalisar.

Duas perdas acontecem dentro do próprio JSON, antes de esta ferramenta entrar, e parecem bugs de conversão. Números JSON são doubles IEEE 754, então um identificador de dezenove dígitos escrito como número puro já perdeu os dígitos finais quando o texto é analisado. E chaves duplicadas são resolvidas pelo analisador, com a última vencendo. Há também uma peculiaridade do JavaScript: chaves que parecem índices de array são enumeradas primeiro e em ordem numérica crescente, então um objeto misturando "2", "10" e "name" não vai emitir os elementos na ordem em que você escreveu.

null e o objeto vazio produzem ambos <x/>, então são indistinguíveis e os dois voltam como string vazia. Se você precisa da distinção, xsi:nil="true" é a única forma amparada pelos padrões de dizer "presente porém nulo", e ela exige o namespace xsi declarado num ancestral.

Fazendo isso em código

A mesma conversão nas quatro linguagens que mais consomem XML, mais PHP e uma linha de shell. As flags de segurança importam na volta: fazer parsing de JSON não é o risco, mas código que converte JSON em XML quase sempre reanalisa esse XML em algum lugar, e os padrões de Java e .NET vão resolver um DOCTYPE se algum aparecer.

import { XMLBuilder } from 'fast-xml-parser';

const builder = new XMLBuilder({
  ignoreAttributes: false,        // default is true: @_ keys would be dropped
  attributeNamePrefix: '@_',
  textNodeName: '#text',
  format: true,
  indentBy: '  ',
  suppressEmptyNode: true,        // write <note/> rather than <note></note>
  processEntities: true,          // escape &, < and " in values
});

const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(data);

// XMLBuilder does not sanitise keys. A key of "2024 total" is written
// verbatim and produces XML that will not parse, so check before building:
const illegal = Object.keys(flatten(data))
  .filter((k) => !/^[A-Za-z_][\w.\-]*(:[A-Za-z_][\w.\-]*)?$/.test(k));
if (illegal.length) throw new Error('Illegal XML names: ' + illegal.join(', '));
import json
import re
import xmltodict


def legal_name(key):
    """Replace illegal characters rather than stripping them, so that
    distinct keys stay distinct. Prefix a leading digit."""
    name = re.sub(r'[^\w.\-:]', '_', key, flags=re.UNICODE)
    return name if re.match(r'^[A-Za-z_:]', name) else '_' + name


def sanitise(node):
    if isinstance(node, dict):
        return {legal_name(k): sanitise(v) for k, v in node.items()}
    if isinstance(node, list):
        return [sanitise(v) for v in node]
    return node


data = json.loads(json_source)
if not isinstance(data, dict) or len(data) != 1:
    data = {'root': data}          # xmltodict.unparse requires a single root

print(xmltodict.unparse(
    sanitise(data),
    pretty=True, indent='  ',
    attr_prefix='@_', cdata_key='#text',
    full_document=True,            # emit the <?xml ...?> declaration
))

# xmltodict raises ValueError("Document must have exactly one root.") rather
# than guessing, which is correct behaviour and the reason for the wrap.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

JsonNode tree = new ObjectMapper().readTree(jsonSource);

XmlMapper xml = new XmlMapper();
xml.enable(SerializationFeature.INDENT_OUTPUT);

// JSON has no root name and Jackson will not invent one, so supply it.
String out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    + xml.writer().withRootName("root").writeValueAsString(tree);

// Two things Jackson will not do for you:
//  1. It does not sanitise names. A key with a space throws
//     IllegalArgumentException at write time, which is at least loud.
//  2. It writes every value as a child element. There is no attribute
//     convention on a JsonNode, so @_ keys become elements unless you bind
//     to a class annotated with @JacksonXmlProperty(isAttribute = true).
using System.Xml;
using Newtonsoft.Json;

// The second argument is the root element name, used when the JSON does not
// already have exactly one top-level property. Without it, multi-key JSON
// throws JsonSerializationException rather than producing invalid XML.
XmlDocument? document = JsonConvert.DeserializeXmlNode(jsonSource, "root");

if (document is null) throw new InvalidOperationException("Empty JSON.");

var settings = new XmlWriterSettings { Indent = true, IndentChars = "  " };
using var writer = XmlWriter.Create(Console.Out, settings);
document.Save(writer);

// Json.NET uses "@" for attributes and "#text" for text, so retarget the
// keys if your JSON came from a converter using "@_". It does not sanitise
// names either: a property called "2024 total" throws XmlException("The ''
// character, hexadecimal value 0x20, cannot be included in a name").
<?php
$data = json_decode($source, true, 512, JSON_THROW_ON_ERROR);

function legal_name(string $key): string {
    $name = preg_replace('/[^\w.\-:]/u', '_', $key);
    return preg_match('/^[A-Za-z_:]/', $name) ? $name : '_' . $name;
}

function write_node(XMLWriter $w, string $name, mixed $value): void {
    if (is_array($value) && array_is_list($value)) {
        foreach ($value as $v) write_node($w, $name, $v);   // repeat, no wrapper
        return;
    }
    $w->startElement(legal_name($name));
    if (is_array($value)) {
        foreach ($value as $k => $v) {
            if (str_starts_with((string) $k, '@_')) {
                $w->writeAttribute(legal_name(substr((string) $k, 2)), (string) $v);
            } elseif ($k === '#text') {
                $w->text((string) $v);
            } else {
                write_node($w, (string) $k, $v);
            }
        }
    } elseif ($value !== null) {
        $w->text(is_bool($value) ? ($value ? 'true' : 'false') : (string) $value);
    }
    $w->endElement();
}

$single = count($data) === 1;
$w = new XMLWriter();
$w->openMemory();
$w->setIndent(true);
$w->setIndentString('  ');
$w->startDocument('1.0', 'UTF-8');
write_node($w, $single ? (string) array_key_first($data) : 'root',
               $single ? reset($data) : $data);
$w->endDocument();
echo $w->outputMemory();
# yq v4 (Mike Farah). JSON is a subset of YAML, so -p=json works directly.
yq -p=json -o=xml '.' payload.json

# Set the root and the key conventions to match this page:
yq -p=json -o=xml \
   --xml-attribute-prefix='@_' \
   --xml-content-name='#text' \
   '{"root": .}' payload.json

# yq writes no XML declaration, so prepend one if a consumer expects it, and
# check the result: yq does not sanitise element names.
{ echo '<?xml version="1.0" encoding="UTF-8"?>'
  yq -p=json -o=xml '{"root": .}' payload.json; } | xmllint --noout --nonet -

Repare no que nenhuma dessas bibliotecas faz: sanitizar uma chave para virar um nome XML legal. Jackson, Json.NET e XMLBuilder ou lançam exceção ou emitem XML que não vai fazer parsing, e o yq emite em silêncio. Se as suas chaves JSON vêm de entrada de usuário, de uma lista de colunas de banco ou de uma linha de cabeçalho de planilha, o passo de sanitização é seu, e substituir caracteres em vez de apagá-los é o que impede duas chaves parecidas de virarem um único elemento.

Perguntas frequentes

Meu JSON sai do navegador?

Não. O analisador de JSON, o sanitizador de nomes e o escritor de XML são todos JavaScript rodando nesta aba, e não existe componente de servidor com quem falar. Abra o painel de Rede e converta algo; nada é requisitado.

É nesta direção que isso mais importa. O JSON colado num conversor costuma ser uma resposta capturada de uma API em produção durante uma depuração, com token de acesso ou um registro completo de cliente. Várias ferramentas bem posicionadas nesta busca enviam esse payload para um servidor, e uma publica documentos salvos numa URL adivinhável.

Por que meu JSON foi envolvido num elemento <root>?

Porque o XML permite exatamente um elemento raiz e o seu JSON tinha mais de uma chave de nível superior, ou era um array, ou um escalar puro. Não há como escrever duas raízes irmãs em XML.

Um objeto com exatamente uma chave é deixado em paz: essa chave vira a raiz e nenhum invólucro é adicionado, então {"order": {...}} dá <order>, enquanto acrescentar uma segunda chave de nível superior dá <root>. O nome do invólucro é editável na linha de controles. Se o XML vai para um lugar que valida, coloque o que o esquema espera.

Como os arrays JSON são convertidos?

Repetindo o nome do elemento uma vez por membro, sem invólucro. {"line": ["a", "b"]} produz dois elementos <line> lado a lado. É assim que o XML representa uma lista, e é o que faz a saída dar a volta. Alguns conversores produzem <line><item>a</item><item>b</item></line>, que lembra mais o JSON e falha a validação contra qualquer esquema escrito para XML de verdade.

Daí saem dois casos-limite. Um array vazio não emite nada, então a chave desaparece, e um array aninhado diretamente dentro de outro achata, porque o interno não tem nome próprio.

O que acontece com chaves que não são nomes de elemento XML válidos?

Elas são renomeadas, e cada renomeação é listada no painel de notas ao lado da saída. Caracteres ilegais são substituídos um a um por sublinhado, e um nome que ainda comece com dígito ganha um sublinhado na frente.

Substituir em vez de remover é deliberado: remover transformaria "2024 total" e "2024total" no mesmo elemento e fundiria dois campos distintos. Ainda assim não é uma injeção perfeita: "first name" e "first_name" viram ambas first_name, porque o sublinhado já era legal na segunda.

Como eu consigo atributos em vez de elementos filhos?

Prefixe a chave com @_. {"user": {"@_id": "7", "name": "Alice"}} produz <user id="7"><name>Alice</name></user>. O prefixo é editável acima do editor; esvazie-o e nada será escrito como atributo.

Coloque ali só escalares. O valor é convertido em string, então um objeto sob uma chave @_ vira o inútil texto [object Object]. Uma coisa que este writer faz e a maioria não: tabulações, quebras de linha e retornos de carro dentro de um valor de atributo são escritos como referências numéricas, então um valor multilinha sobrevive a uma reanálise em vez de virar espaços.

Dá para converter de volta para JSON e recuperar o que eu tinha?

Para a maioria dos documentos, dá, se você usar a página de XML para JSON com as mesmas configurações de @_ e #text. É por causa desse par que aqueles padrões foram escolhidos.

Quatro coisas não sobrevivem. null e {} viram ambos <x/> e voltam como string vazia. Um array vazio desaparece de vez. Os tipos numéricos do JSON somem, porque XML não tem tipos, então 42 volta como "42" a não ser que você ligue a conversão. E a ordem das chaves não é significativa em nenhum dos dois formatos. Se a ida e volta exata é requisito, mantenha o XML como fonte da verdade e leia dele com XPath.

Ferramentas relacionadas

Leitura complementar