Ir para o conteúdo
Allin

De XML para JSON: atributos, repetição e a armadilha do array de um só elemento

Publicado a 20/07/2026 · 15 min de leitura · Ferramentas para programadores

Daniel Okonkwo

Daniel OkonkwoProgramador front-end e redator de Tecnologia na Allin

Desempenho web · Formatos de ficheiro

Verificado a partir de 4 fontes

Ver perfil
Em resumo

O XML não tem forma de dizer que um elemento é uma lista. Os documentos <items><item>a</item></items> e <items><item>a</item><item>b</item></items> só diferem no número de filhos, portanto um conversor que leia um deles não pode saber se item é um elemento repetível que calha ter uma só instância. Esta ferramenta segue a via habitual: um filho devolve {"items":{"item":"a"}}, uma string, e dois devolvem {"items":{"item":["a","b"]}}, um array. Qualquer consumidor que escreva items.item[0] funciona até ao dia em que uma lista tem um elemento, e então lê a letra a — o primeiro carácter da string — em vez do elemento. O mesmo documento pode produzir as duas formas de uma vez: <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> devolve g como array de dois objetos, o primeiro com i como array de dois e o segundo com i como simples string. Existem quatro convenções para enfrentar isto. Declarar a forma num esquema, o único sítio onde a cardinalidade alguma vez é escrita; dar ao conversor uma lista explícita de caminhos que são sempre arrays; prefixar os nomes de atributo para não colidirem com os de elemento; e reservar uma chave para o texto de um elemento que também tem atributos. Esta ferramenta faz as duas últimas: os atributos passam a @nome, e #text leva o texto próprio de um elemento, tenha ele atributos, filhos elemento ou ambos. O conteúdo misto é conservado: <p>Hello <b>world</b>!</p> devolve {"p":{"#text":["Hello","!"],"b":"world"}}, uma entrada de array por cada troço de texto. O que nada regista é o entrelaçamento: a saída não diz que Hello vinha antes de <b> e o ponto de exclamação depois. Os troços que são só espaços são descartados e o resto é aparado, a menos que xml:space="preserve" esteja em vigor, caso em que o espaçamento é guardado tal e qual: <code xml:space="preserve"> keep </code> devolve {"code":{"@xml:space":"preserve","#text":" keep "}}. Todo o valor é uma string: <n>42</n> passa a "42".

Dois documentos que só diferem no número de filhos produzem duas formas JSON diferentes, e nenhum conversor os distingue sem um esquema. Além do que este faz mesmo com os atributos, o conteúdo misto e os espaços — e a única coisa que continua a não conseguir registar.

O defeito está no formato, não no conversor

Escreva em XML uma encomenda com três artigos e repete três vezes o elemento line. Escreva uma encomenda com um só artigo e escreve o elemento uma vez. Nada no documento distingue isso de um elemento que simplesmente não é repetível — não há marca de plural, nem cardinalidade, nem parêntese. A especificação do XML define como é um documento bem formado e não diz absolutamente nada sobre quantas vezes um filho pode aparecer; essa pergunta pertence a um esquema, e um documento não é obrigado a ter um.

Portanto o conversor adivinha, e só há duas maneiras de adivinhar. Produzir sempre um array, o que transforma cada elemento de valor único num array de um e duplica o ruído da saída. Ou produzir um array só quando se vê repetição, que é o que faz quase toda a ferramenta, esta incluída. Dê-lhe <items><item>a</item></items> e devolve {"items":{"item":"a"}}. Dê-lhe <items><item>a</item><item>b</item></items> e devolve {"items":{"item":["a","b"]}}. Dê-lhe <items></items> e devolve {"items":""} — uma string vazia, não um array vazio — porque sem filhos o elemento é tratado como folha e o seu conteúdo textual é nada.

A consequência é que a forma do seu JSON depende dos seus dados, e pode mudar dentro de um mesmo documento. Converta <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> e obtém g como array de dois objetos: no primeiro, i é um array de duas strings; no segundo, i é a string 3. Um código que percorre g e depois indexa i funciona no primeiro elemento e lê em silêncio o carácter 3 no segundo — sem erro, sem falha, só o valor errado a caminho do que vem a seguir. É de longe a maneira mais comum de uma integração XML que funcionava se partir em produção, e parte-se no dia em que os dados encolhem, não no dia em que crescem.

As quatro convenções que existem, e as duas que esta ferramenta usa

A primeira é um esquema. O XML Schema é o único sítio de toda a pilha onde a cardinalidade está escrita: uma declaração de elemento leva minOccurs e maxOccurs, e um maxOccurs maior que um é exatamente a afirmação de que esse elemento é uma lista. Um conversor com o esquema à mão pode produzir corretamente um array de um para um documento que calhe ter um só filho. Sem o esquema, essa informação não existe em lado nenhum do ficheiro, e nenhuma esperteza a recupera.

A segunda é uma lista de caminhos sempre-array entregue ao conversor por quem conhece os dados. A maioria das bibliotecas XML sérias aceita uma: nomeia order.lines.line e passa a ser um array seja qual for a contagem. É um esquema em miniatura, escrito uma vez por um humano que sabe a resposta, e é o remendo pragmático quando não existe esquema formal. Esta ferramenta não tem essa opção, portanto se consumir a sua saída em código, a forma defensiva é normalizar antes de usar: force por si mesmo o valor a array se ainda não o for, e só depois indexe.

A terceira e a quarta são sobre chaves, e esta ferramenta aplica as duas. Os atributos levam o prefixo @, portanto <book id="1"><title>Dune</title></book> devolve {"book":{"@id":"1","title":"Dune"}} e um atributo nunca pode colidir com um filho do mesmo nome: <book title="A"><title>B</title></book> conserva ambos, como @title e title. E #text leva o texto próprio de um elemento assim que esse texto tem de partilhar o objeto com outra coisa, sejam atributos ou filhos elemento: <book id="1" lang="en">Dune</book> devolve {"book":{"@id":"1","@lang":"en","#text":"Dune"}}. Um elemento sem atributos e sem filhos elemento salta a embalagem por completo e passa a ser diretamente o seu texto, e é por isso que <title>Dune</title> é a string Dune e não um objeto de uma chave.

O conteúdo misto vai parar a #text; a ordem, não

O conteúdo misto é um elemento cujos filhos misturam texto e outros elementos: um parágrafo com uma palavra a negrito ao meio, uma descrição com uma ligação em linha, uma cláusula jurídica com um termo destacado. O XML suporta-o e usa-o constantemente; é boa parte daquilo para que serve o XML documental. O JSON não tem sítio natural onde o pôr, porque uma chave de objeto pode conter a palavra a negrito mas não há onde registar que essa palavra estava entre dois trechos de texto.

Este conversor resolve o problema recolhendo o texto em #text. Dê-lhe <p>Hello <b>world</b>!</p> e devolve {"p":{"#text":["Hello","!"],"b":"world"}}: uma entrada por cada troço de texto, na ordem do documento, com a marcação que os separou ao lado do array sob a sua própria chave. Um troço único fica como string simples em vez de array de um: <r>lead<a>1</a></r> dá "#text":"lead". Os atributos não mudam nada: <p id="1">Hello <b>x</b> tail</p> devolve {"p":{"@id":"1","#text":["Hello","tail"],"b":"x"}}. Já não se deita nada fora, portanto um registo ONIX, um fragmento DocBook ou uma descrição RSS com marcação mantêm a sua prosa. O que a forma não lhe pode dizer é onde estava a marcação: nada nesse objeto diz que Hello vinha antes de <b> e o ponto de exclamação depois, e trocar os dois troços na origem produz um JSON idêntico. Se a sequência carrega sentido — uma revisão, uma transcrição, tudo aquilo em que o elemento em linha marca uma posição na frase — guarde o parágrafo como string de XML em vez de o converter.

Os espaços são tratados em duas camadas. Um troço que é só espaços ou quebras de linha entre duas etiquetas é maquetação e não conteúdo, portanto é descartado — é por isso que um documento formatado não enche o seu JSON de strings vazias — e o texto que sobrevive é aparado, de modo que <code> indented line</code> volta como indented line. O XML tem um atributo cuja razão de ser é dizer não faças isso, e o conversor já lhe obedece: <code xml:space="preserve"> keep </code> devolve {"code":{"@xml:space":"preserve","#text":" keep "}}, espaços incluídos. Herda-se como a especificação manda: um preserve num antepassado protege todos os descendentes até que um deles volte a declarar xml:space="default".

Ordem, espaços de nomes e tipos: mais três coisas que não sobrevivem

A ordem do documento entre nomes de elemento diferentes perde-se. <r><a>1</a><b>x</b><a>2</a></r> devolve a como array de 1 e 2, e b como x, o que está correto até aí — os dois elementos a são recolhidos embora não sejam adjacentes — mas o facto de b estar entre eles desapareceu. Num objeto, as chaves não têm uma ordem em que um consumidor se possa apoiar, portanto não há onde registá-la. Para XML em forma de registo, não faz diferença. Para tudo em que a sequência carrega sentido — um registo de fluxo de trabalho, um histórico de alterações, uma narrativa entrelaçada — faz muitíssima, e a conversão perde informação de um modo que nenhuma comparação de tamanhos revela.

Os espaços de nomes são transportados como texto, não compreendidos. Um filho com prefixo conserva o prefixo na chave: <r xmlns:ns="http://example.com"><ns:a>1</ns:a></r> dá a chave ns:a, e a própria declaração aparece como atributo @xmlns:ns. Um espaço de nomes por omissão é declarado como @xmlns e depois os filhos perdem todo o rasto dele, portanto um elemento a num espaço de nomes e um elemento a sem espaço de nomes produzem a mesma chave. Se se juntarem dois vocabulários num documento e ambos usarem o mesmo nome local, obtém uma colisão a que o conversor é cego. Além disso o prefixo é uma escolha do autor, não parte da identidade do elemento: o mesmo documento reserializado com outro prefixo produz chaves JSON diferentes para dados idênticos.

Os tipos não são adivinhados, e aqui a ferramenta acerta. <a><n>42</n><f>1.0</f><z>007</z><t>true</t></a> devolve quatro strings, e não um número, um decimal, uma string com zeros e um booleano. Um documento XML sem esquema também não tem tipos — tudo é dado de carateres — portanto inventá-los seria inventar informação. A consequência prática é que vai comparar com "true" e não com true, e fará por si mesmo a conversão onde precisar de um número. É o arbítrio certo: passar de string a número é uma decisão que exige conhecer o campo, e o conversor não o conhece.

O que este conversor recusa de facto

O analisador por baixo é o do próprio navegador, e é estrito como deve: <br> sozinho é rejeitado, uma etiqueta por fechar é rejeitada, dois elementos raiz são rejeitados e um atributo duplicado é rejeitado. O rigor é justamente o sentido de usar um analisador de XML e não um de HTML, e tudo isso é comportamento correto.

Detetar a falha é mais difícil do que parece, porque um analisador de navegador não lança exceção. Devolve um documento onde enxertou um elemento chamado parsererror, e espera-se que quem chama vá procurá-lo. Procurar só o nome é a implementação óbvia e a errada: recusa qualquer documento válido que traga o seu próprio elemento parsererror, que é exatamente o que contém um registo de compilação ou um relatório de validação. Os motores nem sequer concordam sobre onde fica o marcador: o Firefox enraíza todo o documento num marcador no seu próprio espaço de nomes de erro, ao passo que o Blink e o WebKit injetam um no espaço de nomes XHTML a meio da árvore e deixam a sua raiz no lugar. Por isso a ferramenta pergunta ao motor em vez de adivinhar: uma vez por sessão analisa algo deliberadamente partido, lê o espaço de nomes do marcador que volta e a partir daí olha só para ali. <log><parsererror>none</parsererror><n>1</n></log> converte-se, e devolve {"log":{"parsererror":"none","n":"1"}}.

XML à entrada, JSON à saída — cada linha é a saída que o conversor devolveu de facto
XMLJSON devolvidoO que lhe diz
<items><item>a</item></items>{"items":{"item":"a"}} — uma stringUm filho não é uma lista; indexar [0] devolve o primeiro carácter
<items><item>a</item><item>b</item></items>{"items":{"item":["a","b"]}} — um arrayA forma da saída depende da contagem, não do vocabulário
<items></items> ou <items/>{"items":""} — uma string vaziaNem array vazio nem null: três estados reduzem-se a um
<book id="1" lang="en">Dune</book>{"book":{"@id":"1","@lang":"en","#text":"Dune"}}Os atributos levam @, o texto próprio leva #text: nenhum tapa um filho
<p>Hello <b>world</b>!</p>{"p":{"#text":["Hello","!"],"b":"world"}} — uma entrada por troço de textoA prosa sobrevive; o entrelaçamento não: nada diz que Hello vinha antes de <b>
<code xml:space="preserve"> keep </code>{"code":{"@xml:space":"preserve","#text":" keep "}}A instrução é respeitada e herdada; sem ela, cada troço é aparado
<log><parsererror>none</parsererror><n>1</n></log>{"log":{"parsererror":"none","n":"1"}} — convertido, não recusadoA verificação de falha pergunta ao motor que espaço de nomes usa o seu marcador, portanto o seu elemento está a salvo
XML para JSONConverte XML — colado ou de um ficheiro — em JSON limpo e legível.Experimentar a ferramenta

Perguntas frequentes

Como escrevo código que sobreviva a uma lista de um?
Normalize antes de ler. Onde quer que o seu código espere uma lista, force primeiro o valor: se já for um array, mantenha-o; se não, embrulhe-o; e trate um valor ausente ou vazio como o array vazio. Três linhas na fronteira do seu analisador, aplicadas a cada caminho que saiba ser repetível, e o caso de um só elemento deixa de existir para o resto do programa. Fazê-lo na fronteira conta mais do que o código exato: uma coerção polvilhada em cada ponto de uso acabará esquecida num deles, e será o que corre no fecho do mês. Se consome o mesmo fluxo com regularidade, escreva a lista de caminhos repetíveis como constante ao lado do analisador, para que o próximo veja que forma esperava.
Porque não produzir sempre um array?
Porque é ilegível, e a legibilidade é quase tudo aquilo por que se converte XML em JSON. Embrulhe cada elemento e um registo com quinze campos de valor único passa a quinze arrays de um, cada um para desembrulhar à mão antes de poder ser impresso. As bibliotecas que suportam essa estratégia costumam torná-la opcional caminho a caminho por essa mesma razão. Há também um custo mais subtil: um array de um afirma que o elemento é repetível, e se não for, deixou por escrito um facto falso sobre o vocabulário. Entre os dois erros, um conversor que adivinha a partir dos dados pelo menos nunca mente sobre o documento que lhe deram — apenas lhe diz menos do que precisava.
O que acontece ao texto à volta da minha etiqueta <b>?
Vai para #text, ao lado do elemento filho em vez de à volta dele. <p>Hello <b>world</b>!</p> devolve {"p":{"#text":["Hello","!"],"b":"world"}}: uma entrada por troço, na ordem do documento, e uma string simples em vez de um array quando há um só troço. Os troços que são só espaços são descartados, a menos que xml:space="preserve" esteja em vigor. O que não recupera é o entrelaçamento: o JSON não lhe pode dizer que Hello vinha antes da palavra a negrito e o ponto de exclamação depois, e se um parágrafo tiver três troços e três elementos em linha, recompor a frase a partir desse objeto é adivinhação. Se só precisa da prosa, leia #text e junte os troços. Se precisa da frase exatamente como foi escrita, não converta o parágrafo: guarde-o como string de XML, ou use um conversor feito para conteúdo misto, que representa os filhos de um elemento como um único array ordenado de nós de texto e de elemento em vez de chaves de objeto.
O conversor lida com CDATA e entidades?
Sim, ambos, e corretamente. Uma secção CDATA é desembrulhada e o seu conteúdo passa a texto ordinário, portanto <a><![CDATA[<not>markup</not>]]></a> devolve a string <not>markup</not> — os sinais de menor e maior sobrevivem como carateres, que é todo o sentido do CDATA. As entidades nomeadas são resolvidas, portanto &amp; e &lt; voltam como o e comercial e o sinal de menor, e as referências numéricas de carácter também: caf&#233; devolve café. Os comentários e as instruções de processamento, incluindo a própria declaração XML, são eliminados por completo, já que não são conteúdo de elemento. Nada disto é obra do conversor: é o analisador de XML do navegador a fazer o que a especificação diz, boa razão para preferir um analisador a sério a uma expressão regular em tudo o que passe de um fluxo fixo que controla.
O meu documento está bem formado mas a ferramenta diz que é inválido. E agora?
Verifique duas coisas, por esta ordem. Primeiro, procure hábitos de HTML que o XML não permite — um <br> ou um <img> sem barra de fecho, um e comercial sem escape num URL, um atributo duplicado num elemento, ou um carácter perdido antes da declaração XML, como uma marca de ordem de bytes chegada por um copiar e colar. Segundo, confirme que há exatamente um elemento raiz: dois irmãos no nível de topo são um fragmento, não um documento, e precisam de ser embrulhados para que alguma coisa os analise. Se as duas estiverem limpas e continuar recusado, passe-o por um validador de XML, que lhe apontará uma linha em vez de lhe dizer apenas que algo está mal.

Artigos que podem interessar-lhe

Todos os guias

Ferramentas relacionadas

Isto descreve o que estes conversores fazem hoje, verificado executando-os, e não o que uma norma obrigue um conversor a fazer. O CSV não tem norma prescritiva: o RFC 4180 é informativo e descreve uma prática corrente, pelo que duas ferramentas aparentemente corretas podem divergir sobre o mesmo ficheiro sem que nenhuma esteja errada. O achatamento, a deteção de tipos e a de arrays são convenções, não regras. Antes de converter dados que não possa reexportar, passe primeiro por uma cópia e compare o número de linhas e colunas nas duas pontas.

Fontes

Detetaste um erro neste artigo?