Vai al contenuto
Allin

Da XML a JSON: attributi, ripetizione e la trappola dell'array a un solo elemento

Pubblicato il 20/07/2026 · 14 min di lettura · Strumenti per sviluppatori

Daniel Okonkwo

Daniel OkonkwoSviluppatore front-end e redattore Tech presso Allin

Performance web · Formati di file

Verificato su 4 fonti

Vedi il profilo
In breve

L'XML non ha modo di dire che un elemento è una lista. I documenti <items><item>a</item></items> e <items><item>a</item><item>b</item></items> differiscono solo per quanti figli esistono, quindi un convertitore che ne legge uno non può sapere se item sia un elemento ripetibile che per caso ha una sola istanza. Questo strumento prende la via consueta: un figlio restituisce {"items":{"item":"a"}}, una stringa, e due restituiscono {"items":{"item":["a","b"]}}, un array. Qualsiasi consumatore che scriva items.item[0] funziona fino al giorno in cui una lista ha un elemento, e allora legge la lettera a — il primo carattere della stringa — invece della voce. Lo stesso documento può produrre entrambe le forme insieme: <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> restituisce g come array di due oggetti, il primo con i come array di due e il secondo con i come semplice stringa. Esistono quattro convenzioni per affrontarlo. Dichiarare la forma in uno schema, l'unico posto in cui la cardinalità venga mai scritta; dare al convertitore un elenco esplicito di percorsi che sono sempre array; anteporre un prefisso ai nomi di attributo perché non collidano con quelli di elemento; e riservare una chiave per il testo di un elemento che porta anche attributi. Questo strumento fa le ultime due: gli attributi diventano @nome, e #text porta il testo proprio di un elemento, che questo abbia attributi, figli elemento o entrambi. Il contenuto misto è conservato: <p>Hello <b>world</b>!</p> restituisce {"p":{"#text":["Hello","!"],"b":"world"}}, una voce di array per ogni tratto di testo. Ciò che nulla registra è l'intreccio: l'output non dice che Hello veniva prima di <b> e il punto esclamativo dopo. I tratti fatti solo di spazi vengono scartati e il resto ripulito, a meno che xml:space="preserve" sia in vigore, nel qual caso la spaziatura è mantenuta identica: <code xml:space="preserve"> keep </code> restituisce {"code":{"@xml:space":"preserve","#text":" keep "}}. Ogni valore è una stringa: <n>42</n> diventa "42".

Due documenti che differiscono solo per quanti figli esistono producono due forme JSON diverse, e senza uno schema nessun convertitore può distinguerli. Più ciò che questo fa davvero con attributi, contenuto misto e spazi — e l'unica cosa che continua a non poter registrare.

Il difetto è nel formato, non nel convertitore

Scrivi in XML un ordine con tre articoli e ripeti tre volte l'elemento line. Scrivi un ordine con un solo articolo e scrivi l'elemento una volta. Nulla nel documento distingue questo da un elemento che semplicemente non è ripetibile: non c'è marca di plurale, né cardinalità, né parentesi. La specifica XML definisce com'è fatto un documento ben formato e non dice assolutamente nulla su quante volte un figlio possa comparire; quella domanda appartiene a uno schema, e un documento non è obbligato ad averne uno.

Quindi il convertitore tira a indovinare, e ci sono solo due modi di farlo. Produrre sempre un array, il che trasforma ogni elemento a valore unico in un array da uno e raddoppia il rumore nell'output. Oppure produrre un array solo quando si vede una ripetizione, che è ciò che fa quasi ogni strumento, questo compreso. Dagli <items><item>a</item></items> e restituisce {"items":{"item":"a"}}. Dagli <items><item>a</item><item>b</item></items> e restituisce {"items":{"item":["a","b"]}}. Dagli <items></items> e restituisce {"items":""} — una stringa vuota, non un array vuoto — perché senza figli l'elemento è trattato come foglia e il suo contenuto testuale è nulla.

La conseguenza è che la forma del tuo JSON dipende dai tuoi dati, e può cambiare all'interno di un unico documento. Converti <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> e ottieni g come array di due oggetti: nel primo, i è un array di due stringhe; nel secondo, i è la stringa 3. Un codice che percorre g e poi indicizza i funziona sul primo elemento e legge in silenzio il carattere 3 sul secondo — nessun errore, nessun crash, solo il valore sbagliato che prosegue. È di gran lunga il modo più comune in cui un'integrazione XML funzionante si rompe in produzione, e si rompe il giorno in cui i dati si rimpiccioliscono, non quando crescono.

Le quattro convenzioni esistenti, e le due che usa questo strumento

La prima è uno schema. XML Schema è l'unico punto dell'intera pila in cui la cardinalità sia scritta: una dichiarazione di elemento porta minOccurs e maxOccurs, e un maxOccurs maggiore di uno è esattamente l'affermazione che quell'elemento è una lista. Un convertitore che abbia lo schema può produrre correttamente un array da uno per un documento che si trovi ad avere un solo figlio. Senza lo schema, quell'informazione non esiste in nessun punto del file, e nessuna astuzia la recupera.

La seconda è un elenco di percorsi sempre-array consegnato al convertitore da chi conosce i dati. La maggior parte delle librerie XML serie ne accetta uno: nomini order.lines.line e diventa un array qualunque sia il conteggio. È uno schema in miniatura, scritto una volta da un umano che conosce la risposta, ed è la soluzione pragmatica quando non esiste uno schema formale. Questo strumento non ha quell'opzione, quindi se consumi il suo output nel codice, la forma difensiva è normalizzare prima di usarlo: forza tu il valore ad array se non lo è già, e poi indicizza.

La terza e la quarta riguardano le chiavi, e questo strumento le applica entrambe. Gli attributi hanno il prefisso @, quindi <book id="1"><title>Dune</title></book> restituisce {"book":{"@id":"1","title":"Dune"}} e un attributo non può mai collidere con un figlio dello stesso nome: <book title="A"><title>B</title></book> conserva entrambi, come @title e title. E #text porta il testo proprio di un elemento non appena quel testo deve condividere l'oggetto con qualcos'altro, siano attributi o figli elemento: <book id="1" lang="en">Dune</book> restituisce {"book":{"@id":"1","@lang":"en","#text":"Dune"}}. Un elemento senza attributi e senza figli elemento salta del tutto l'involucro e diventa direttamente il suo testo, ed è per questo che <title>Dune</title> è la stringa Dune e non un oggetto con una chiave.

Il contenuto misto finisce in #text, l'ordine no

Il contenuto misto è un elemento i cui figli mescolano testo e altri elementi: un paragrafo con una parola in grassetto nel mezzo, una descrizione con un link in linea, una clausola legale con un termine evidenziato. L'XML lo supporta e lo usa di continuo; è gran parte di ciò a cui serve l'XML documentale. Il JSON non ha un posto naturale dove metterlo, perché una chiave di oggetto può contenere la parola in grassetto ma non c'è dove registrare che quella parola stava fra due tratti di testo.

Questo convertitore risolve il problema raccogliendo il testo in #text. Dagli <p>Hello <b>world</b>!</p> e restituisce {"p":{"#text":["Hello","!"],"b":"world"}}: una voce per ogni tratto di testo, nell'ordine del documento, con il markup che li ha separati accanto all'array sotto la propria chiave. Un tratto singolo resta una semplice stringa invece di un array di uno: <r>lead<a>1</a></r> dà "#text":"lead". Gli attributi non cambiano nulla: <p id="1">Hello <b>x</b> tail</p> restituisce {"p":{"@id":"1","#text":["Hello","tail"],"b":"x"}}. Non si butta più via niente, quindi un record ONIX, un frammento DocBook o una descrizione RSS con markup mantengono la loro prosa. Ciò che la forma non può dirti è dove stava il markup: nulla in quell'oggetto dice che Hello veniva prima di <b> e il punto esclamativo dopo, e scambiare i due tratti nella sorgente produce un JSON identico. Se la sequenza porta significato — una revisione, una trascrizione, tutto ciò in cui l'elemento in linea segna una posizione nella frase — tieni il paragrafo come stringa di XML invece di convertirlo.

Gli spazi sono trattati su due livelli. Un tratto fatto solo di spazi o a capo tra due tag è impaginazione e non contenuto, quindi viene scartato — ecco perché un documento formattato non riempie il tuo JSON di stringhe vuote — e il testo che sopravvive viene ripulito, così <code> indented line</code> torna come indented line. L'XML ha un attributo la cui unica ragione d'essere è dire non farlo, e il convertitore ora gli obbedisce: <code xml:space="preserve"> keep </code> restituisce {"code":{"@xml:space":"preserve","#text":" keep "}}, spazi compresi. Si eredita come prevede la specifica: un preserve su un antenato protegge ogni discendente finché uno di essi non dichiara di nuovo xml:space="default".

Ordine, spazi dei nomi e tipi: altre tre cose che non sopravvivono

L'ordine del documento fra nomi di elemento diversi si perde. <r><a>1</a><b>x</b><a>2</a></r> restituisce a come array di 1 e 2, e b come x, il che è corretto fin lì — i due elementi a vengono raccolti benché non siano adiacenti — ma il fatto che b stesse in mezzo è sparito. In un oggetto le chiavi non hanno un ordine su cui un consumatore possa contare, quindi non c'è dove annotarlo. Per XML a forma di record non importa. Per tutto ciò in cui la sequenza porta significato — un registro di flusso di lavoro, uno storico di modifiche, una narrazione intrecciata — importa moltissimo, e la conversione perde informazione in un modo che nessun confronto di dimensioni rivela.

Gli spazi dei nomi sono trasportati come testo, non compresi. Un figlio con prefisso conserva il prefisso nella chiave: <r xmlns:ns="http://example.com"><ns:a>1</ns:a></r> dà la chiave ns:a, e la dichiarazione stessa compare come attributo @xmlns:ns. Uno spazio dei nomi predefinito è dichiarato come @xmlns e poi i figli ne perdono ogni traccia, quindi un elemento a in uno spazio dei nomi e un elemento a senza spazio dei nomi producono la stessa chiave. Se due vocabolari sono uniti in un documento ed entrambi usano lo stesso nome locale, ottieni una collisione a cui il convertitore è cieco. Il prefisso è inoltre una scelta dell'autore, non parte dell'identità dell'elemento: lo stesso documento riserializzato con un prefisso diverso produce chiavi JSON differenti per dati identici.

I tipi non vengono indovinati, e qui lo strumento ha ragione. <a><n>42</n><f>1.0</f><z>007</z><t>true</t></a> restituisce quattro stringhe, non un numero, un decimale, una stringa con zeri e un booleano. Un documento XML senza schema non ha tipi nemmeno lui — tutto è dato carattere — quindi inventarli sarebbe inventare informazione. Conseguenza pratica: confronterai con "true" e non con true, e farai tu la conversione dove ti serve un numero. È il compromesso giusto: passare da stringa a numero è una decisione che richiede di conoscere il campo, e il convertitore non lo conosce.

Che cosa rifiuta davvero questo convertitore

Il parser sottostante è quello del browser, ed è rigoroso nel modo giusto: <br> da solo è rifiutato, un tag non chiuso è rifiutato, due elementi radice sono rifiutati e un attributo duplicato è rifiutato. Il rigore è proprio il senso di usare un parser XML invece di uno HTML, e tutto questo è comportamento corretto.

Rilevare il fallimento è più difficile di quanto sembri, perché un parser di browser non solleva un'eccezione. Restituisce un documento in cui ha innestato un elemento chiamato parsererror, e ci si aspetta che sia il chiamante a cercarlo. Cercare il solo nome è l'implementazione ovvia e quella sbagliata: rifiuta qualsiasi documento valido che porti un proprio elemento parsererror, che è esattamente ciò che contiene un log di build o un rapporto di validazione. I motori non concordano nemmeno su dove finisca il marcatore: Firefox radica l'intero documento in uno nel proprio spazio dei nomi di errore, mentre Blink e WebKit ne iniettano uno nello spazio dei nomi XHTML a metà albero e lasciano la tua radice al suo posto. Perciò lo strumento interroga il motore invece di tirare a indovinare: una volta per sessione analizza qualcosa di deliberatamente rotto, legge lo spazio dei nomi del marcatore che torna, e da lì in poi guarda solo là. <log><parsererror>none</parsererror><n>1</n></log> si converte, e restituisce {"log":{"parsererror":"none","n":"1"}}.

XML in ingresso, JSON in uscita — ogni riga è l'output realmente restituito dal convertitore
XMLJSON restituitoChe cosa ti dice
<items><item>a</item></items>{"items":{"item":"a"}} — una stringaUn figlio non è una lista; indicizzare [0] restituisce il primo carattere
<items><item>a</item><item>b</item></items>{"items":{"item":["a","b"]}} — un arrayLa forma dell'output dipende dal conteggio, non dal vocabolario
<items></items> o <items/>{"items":""} — una stringa vuotaNé array vuoto né null: tre stati collassano in uno
<book id="1" lang="en">Dune</book>{"book":{"@id":"1","@lang":"en","#text":"Dune"}}Gli attributi prendono @, il testo proprio prende #text: nessuno oscura un figlio
<p>Hello <b>world</b>!</p>{"p":{"#text":["Hello","!"],"b":"world"}} — una voce per tratto di testoLa prosa sopravvive; l'intreccio no: nulla dice che Hello veniva prima di <b>
<code xml:space="preserve"> keep </code>{"code":{"@xml:space":"preserve","#text":" keep "}}L'istruzione è rispettata ed ereditata; senza di essa ogni tratto viene ripulito
<log><parsererror>none</parsererror><n>1</n></log>{"log":{"parsererror":"none","n":"1"}} — convertito, non rifiutatoIl controllo di fallimento chiede al motore quale spazio dei nomi usa il suo marcatore, così il tuo elemento è al sicuro
XML in JSONConverti XML — incollato o da un file — in JSON pulito e leggibile.Prova lo strumento

Domande frequenti

Come scrivo codice che sopravviva a una lista di uno?
Normalizza prima di leggere. Ovunque il tuo codice si aspetti una lista, forza prima il valore: se è già un array tienilo, altrimenti avvolgilo, e tratta un valore assente o vuoto come l'array vuoto. Tre righe al confine del tuo parser, applicate a ogni percorso che sai essere ripetibile, e il caso a un elemento smette di esistere per tutto il resto del programma. Farlo al confine conta più del codice esatto: una coercizione sparsa a ogni punto d'uso prima o poi verrà dimenticata in uno di essi, e sarà quello che gira a fine mese. Se consumi lo stesso flusso con regolarità, scrivi l'elenco dei percorsi ripetibili come costante accanto al parser, così il prossimo vede quale forma ti aspettavi.
Perché non produrre sempre un array?
Perché è illeggibile, e la leggibilità è quasi tutto ciò per cui si converte XML in JSON. Avvolgi ogni elemento e un record con quindici campi a valore unico diventa quindici array da uno, ciascuno da scartare a mano prima di poter essere stampato. Le librerie che supportano questa strategia la rendono di solito opzionale percorso per percorso proprio per questo. C'è anche un costo più sottile: un array da uno afferma che l'elemento è ripetibile, e se non lo è, hai messo per iscritto un fatto falso sul vocabolario. Fra i due errori, un convertitore che indovina dai dati almeno non mente mai sul documento che gli è stato dato: ti dice soltanto meno di quanto ti serviva.
Che fine fa il testo attorno al mio tag <b>?
Finisce in #text, accanto all'elemento figlio anziché attorno a esso. <p>Hello <b>world</b>!</p> restituisce {"p":{"#text":["Hello","!"],"b":"world"}}: una voce per tratto, nell'ordine del documento, e una semplice stringa invece di un array quando il tratto è uno solo. I tratti fatti solo di spazi vengono scartati, a meno che xml:space="preserve" sia in vigore. Ciò che non riprendi è l'intreccio: il JSON non può dirti che Hello veniva prima della parola in grassetto e il punto esclamativo dopo, e se un paragrafo ha tre tratti e tre elementi in linea, ricomporre la frase da quell'oggetto è tirare a indovinare. Se ti serve solo la prosa, leggi #text e unisci i tratti. Se ti serve la frase esattamente com'è stata scritta, non convertire affatto il paragrafo: tienilo come stringa di XML, oppure usa un convertitore pensato per il contenuto misto, che rappresenta i figli di un elemento come un unico array ordinato di nodi di testo ed elemento invece che come chiavi di oggetto.
Il convertitore gestisce CDATA ed entità?
Sì, entrambi, e correttamente. Una sezione CDATA viene scartata come involucro e il suo contenuto diventa testo ordinario, quindi <a><![CDATA[<not>markup</not>]]></a> restituisce la stringa <not>markup</not>: le parentesi angolari sopravvivono come caratteri, che è tutto il senso del CDATA. Le entità con nome sono risolte, quindi &amp; e &lt; tornano come e commerciale e segno di minore, e anche i riferimenti numerici di carattere: caf&#233; restituisce café. I commenti e le istruzioni di elaborazione, compresa la dichiarazione XML stessa, sono eliminati del tutto, non essendo contenuto di elemento. Nulla di ciò è opera del convertitore: è il parser XML del browser che fa ciò che dice la specifica, buona ragione per preferire un parser vero a un'espressione regolare in tutto ciò che vada oltre un flusso fisso che controlli.
Il mio documento è ben formato ma lo strumento dice che non è valido. E adesso?
Controlla due cose, in quest'ordine. Primo, cerca abitudini da HTML che l'XML non ammette: un <br> o un <img> senza barra di chiusura, una e commerciale non protetta in un URL, un attributo duplicato su un elemento, o un carattere vagante prima della dichiarazione XML, come un byte order mark arrivato con un copia e incolla. Secondo, verifica che ci sia esattamente un elemento radice: due fratelli al livello superiore sono un frammento, non un documento, e vanno avvolti prima che qualcosa li analizzi. Se entrambi sono puliti e il rifiuto persiste, passalo in un validatore XML, che ti indicherà una riga invece di dirti solo che qualcosa non va.

Articoli che potrebbero interessarti

Tutte le guide

Strumenti correlati

Questo descrive ciò che questi convertitori fanno oggi, verificato eseguendoli, e non ciò che una norma imponga a un convertitore. Il CSV non ha una norma prescrittiva: la RFC 4180 è informativa e descrive una prassi diffusa, perciò due strumenti apparentemente corretti possono divergere sullo stesso file senza che nessuno sbagli. L'appiattimento, il riconoscimento dei tipi e quello degli array sono convenzioni, non regole. Prima di convertire dati che non potrai riesportare, passa prima su una copia e confronta il numero di righe e colonne alle due estremità.

Fonti

Hai notato un errore in questo articolo?