Skip to content
Torna al blog
Tutorial

Da XML a JSON online: convenzioni, insidie ed esempi di codice

Converti XML in JSON: come vengono mappati attributi, array e namespace, perché i valori restano stringhe, con codice JavaScript, Python e browser.

13 min di lettura

Conversione da XML a JSON: convenzioni, insidie ed esempi di codice

Recuperi una risposta da un endpoint SOAP, da un feed RSS o da un sitemap.xml, ed è XML. Il tuo stack invece parla JSON nativamente: JavaScript sul front end, REST in mezzo, un document store in fondo. Tocca convertire l’XML in JSON, allora prendi un parser convinto che basti una riga.

E di solito basta, finché l’output non ti morde. Un array che ti aspettavi si rivela un singolo oggetto. Un attributo id sparisce. Un CAP come 01234 torna indietro come il numero 1234. Nessuno di questi è un bug del tuo parser: nascono dalla mappatura di due modelli di dati che non combaciano, e l’unico modo per convertire l’XML in JSON in modo affidabile è capire le convenzioni che colmano il divario.

Questa guida spiega perché quelle convenzioni esistono, quattro modi per fare la conversione (browser, JavaScript, Python, CLI), le regole @_ e #text che ogni libreria principale condivide, le insidie che causano perdita silenziosa di dati e come riconvertire JSON in XML per un round-trip pulito. Gli esempi sono reali ed eseguibili: incollali in Node, Python o in una shell e producono l’output mostrato nei commenti.

Perché la conversione XML-JSON ha bisogno di convenzioni (non di una semplice riformattazione)

A prima vista XML e JSON si somigliano, entrambi sono alberi di dati nominati e annidati, ma i modelli sottostanti divergono in modi che contano. Gli elementi XML possono portare attributi, contenere contenuto misto (testo intervallato a elementi figli) e vivere sotto namespace. JSON non ha niente di tutto questo. Ha oggetti, array e quattro tipi scalari. Convertire uno nell’altro non è riformattare: è tradurre tra due grammatiche dove una ha parole che l’altra non sa scrivere.

Prima di convertire qualcosa, conviene confermare che la sorgente sia davvero valida. Una & non escapata o un tag non corrispondente verranno rifiutati dal parser, quindi far passare l’input attraverso un Formattatore XML per verificarne prima la well-formedness ti risparmia un giro di errori confusi.

Ecco dove i due modelli si allontanano:

DimensioneXMLJSON
Tipi di nodoelementi, attributi, testo, contenuto mistooggetti, array, stringa, numero, booleano, null
Vincolo di rootrichiesto esattamente un elemento rootnessun vincolo di root
Attributisì (id="P01")nessuno (serve una convenzione @_)
Elementi ripetutifratelli con lo stesso nome sono lecitile chiavi di un oggetto non possono ripetersi (serve una convenzione array)
Sistema di tipiil testo è senza tipo: tutto è una stringatipi nativi
Namespacesì (xmlns)nessuno

Poiché i modelli non coincidono, ogni conversione da XML a JSON è guidata dalle convenzioni, non è una riformattazione senza perdite. Per fortuna le convenzioni non sono arbitrarie: fast-xml-parser (Node.js), xmltodict (Python) e JAXB (Java) hanno convergito sugli stessi due marcatori, @_ per gli attributi e #text per il testo del contenuto misto. Imparale una volta e le ritrovi da un runtime all’altro. È lo stesso motivo per cui le discrepanze di forma dei dati emergono anche in altre conversioni, come le questioni di inferenza dei tipi nella guida alla conversione da CSV a JSON.

Come convertire XML in JSON: 4 metodi

Scegli il metodo adatto al tuo contesto: un incollaggio rapido una tantum, un servizio Node, una pipeline Python o uno script shell in CI.

Metodo 1 — Strumento basato su browser (zero configurazione, privacy prima di tutto)

Per una conversione una tantum, o per XML che preferiresti non incollare in un sito qualsiasi, un convertitore in-browser è la strada più veloce. Incolla l’XML nel Convertitore XML in JSON e il JSON appare subito: niente installazione, niente account, niente upload. Tutto gira nel motore JavaScript del tuo browser, quindi i dati non lasciano mai la macchina.

Quest’ultimo dettaglio conta più di quanto sembri. Le buste SOAP trasportano token WS-Security, le configurazioni interne contengono stringhe di connessione e gli export portano con sé record di clienti. Dato che non viene trasmesso nulla, lo strumento è sicuro anche per XML che contiene credenziali o payload sensibili. Puoi verificarlo tu stesso: apri la scheda Network e osserva che mentre converti non parte nessuna richiesta.

Metodo 2 — JavaScript / Node.js (fast-xml-parser)

In Node, fast-xml-parser è la scelta standard. I valori predefiniti però ti sorprenderanno: gli attributi vengono ignorati e i valori subiscono coercizione, quindi per una conversione fedele ti servono le opzioni qui sotto:

// Converte XML in JSON in Node.js usando fast-xml-parser
import { XMLParser } from 'fast-xml-parser';

const xml = `<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>`;

const parser = new XMLParser({
  ignoreAttributes: false,    // mantiene gli attributi (il default li scarta!)
  attributeNamePrefix: '@_',  // gli attributi diventano chiavi con prefisso @_
  textNodeName: '#text',      // il testo del contenuto misto finisce sotto #text
  parseAttributeValue: false, // nessuna coercizione di tipo sugli attributi
  parseTagValue: false,       // nessuna coercizione di tipo sul testo degli elementi
});

const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
//   "catalog": {
//     "product": {
//       "@_id": "P01",
//       "name": "Wireless Headphones",
//       "price": {
//         "@_currency": "USD",
//         "#text": "79.99"
//       }
//     }
//   }
// }

Le due impostazioni che quasi tutti dimenticano sono ignoreAttributes: false e parseTagValue: false. La prima mantiene i tuoi attributi id e currency; la seconda impedisce al parser di trasformare "79.99" in un float e "01234" in 1234. Sul perché conservare le stringhe sia il default sicuro torniamo nella sezione delle insidie.

Se vuoi zero dipendenze nel browser, il DOMParser nativo fa il parsing al posto tuo e percorri il DOM a mano:

// XML a JSON senza dipendenze nel browser usando DOMParser
function xmlToJson(node) {
  // Elemento solo-testo → valore stringa
  const children = Array.from(node.children);
  if (children.length === 0 && node.attributes.length === 0) {
    return node.textContent.trim();
  }

  const obj = {};
  // Attributi → prefisso @_
  for (const attr of node.attributes) {
    obj['@_' + attr.name] = attr.value;
  }
  // Elemento con attributi E testo → #text
  if (children.length === 0) {
    obj['#text'] = node.textContent.trim();
    return obj;
  }
  // Ricorre nei figli, raccogliendo i fratelli con lo stesso nome in array
  for (const child of children) {
    const value = xmlToJson(child);
    if (obj[child.tagName] === undefined) {
      obj[child.tagName] = value;
    } else {
      if (!Array.isArray(obj[child.tagName])) obj[child.tagName] = [obj[child.tagName]];
      obj[child.tagName].push(value);
    }
  }
  return obj;
}

const doc = new DOMParser().parseFromString(
  '<catalog><product id="P01"><name>Wireless Headphones</name></product></catalog>',
  'text/xml'
);
const json = { [doc.documentElement.tagName]: xmlToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));
// { "catalog": { "product": { "@_id": "P01", "name": "Wireless Headphones" } } }

DOMParser è conforme a XML 1.0, gestisce CDATA e riferimenti a entità e segnala gli errori di well-formedness, il tutto senza installare alcun pacchetto. Lo scotto è che la logica di traversamento la scrivi tu, regola di raccolta in array compresa.

Metodo 3 — Python (xmltodict)

In Python, xmltodict riduce tutto a una breve pipeline. Di default usa @ come prefisso per gli attributi e #text per il contenuto misto:

# Converte XML in JSON in Python usando xmltodict
import json
import xmltodict

xml = """<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>"""

data = xmltodict.parse(xml)
print(json.dumps(data, indent=2))
# {
#   "catalog": {
#     "product": {
#       "@id": "P01",
#       "name": "Wireless Headphones",
#       "price": {
#         "@currency": "USD",
#         "#text": "79.99"
#       }
#     }
#   }
# }

Di default xmltodict mantiene ogni valore come stringa, ed è proprio il comportamento che vuoi. L’unica opzione da conoscere fin da subito è force_list, che risolve il problema array singolo-contro-molti prima che arrivi al tuo codice:

# force_list garantisce che <product> sia sempre una lista, anche quando ce n'è uno
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product']  # adesso è sempre una lista
for p in products:
    print(p['name'])

Senza force_list, un solo <product> produce un dict e due una lista, e il tuo loop va in crash quando l’elemento è uno solo. È l’insidia #1, che vediamo qui sotto.

Metodo 4 — CLI (yq / one-liner Python)

Per gli script shell e le pipeline CI, due one-liner coprono la maggior parte dei casi. Lo yq di Mike Farah legge XML ed emette JSON direttamente:

# Usando yq (la versione Go di Mike Farah)
yq -p=xml -o=json '.' input.xml

# Pipe da stdin
cat sitemap.xml | yq -p=xml -o=json '.'

Se xmltodict è già nel tuo ambiente, l’one-liner Python non richiede alcun binario aggiuntivo:

python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml

Entrambi leggono in streaming da stdin, quindi si infilano direttamente in una pipeline: comodo per convertire una risposta API a metà script o normalizzare un batch di file in uno step di build.

Le convenzioni dell’attributo @_ e di #text spiegate

La maggior parte delle pagine di conversione salta la parte che conta davvero: cosa significano le strane chiavi @_ e #text e perché esistono. Chiarito questo, l’output smette di sembrare arbitrario.

Gli attributi vengono mappati su chiavi con prefisso @_. Un attributo non ha equivalente in JSON: in un oggetto non esiste uno slot per “metadati su questo oggetto” distinto da un figlio. La convenzione è dare agli attributi una chiave con prefisso @_:

<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }

Perché proprio @_? Perché nessun nome di elemento XML valido può iniziare con @, quindi il prefisso non collide mai con la chiave di un elemento figlio reale. È un namespace riservato in bella vista. (xmltodict usa il semplice @; fast-xml-parser usa @_ di default. Il principio è identico.)

Il contenuto misto viene mappato su #text. Quando un elemento ha sia un attributo sia un valore testuale, il testo ha bisogno di un posto dove vivere accanto alle chiavi degli attributi. Quel posto è #text:

<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }

Gli elementi di solo testo diventano un valore stringa diretto. Nessun attributo, nessun figlio, solo testo: l’indirezione #text non serve. <name>Alice</name> diventa "name": "Alice". La chiave #text compare solo quando gli attributi costringono il valore dell’elemento a essere un oggetto.

Questa asimmetria è all’origine di un bug sottile. Lo stesso nome di elemento può dare una stringa semplice in un documento e un oggetto @_/#text in un altro, a seconda che quella particolare istanza portasse un attributo. Un <price> senza attributo currency è la stringa "29.99"; lo stesso <price currency="USD"> è { "@_currency": "USD", "#text": "29.99" }. Il codice che legge node.price direttamente funziona per una forma e si rompe in silenzio sull’altra. L’accessor difensivo verifica il tipo: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.

Il CDATA diventa contenuto di testo semplice. Una sezione <![CDATA[if (a < b) return;]]> è solo un meccanismo di escaping, quindi i delimitatori cadono e resta il testo interno: "if (a < b) return;". Nel JSON non sopravvive niente di speciale.

Una volta ottenuto l’output, incollalo in un Formattatore JSON per validarlo e confermare che la struttura corrisponda a quanto si aspetta il tuo consumatore, prima di cablarla nel codice.

5 insidie della conversione XML-JSON e come evitarle

Questi sono i fallimenti che passano la code review e poi saltano fuori in produzione. Ognuno risale alla discrepanza dei modelli vista all’inizio della guida.

1. Ambiguità degli array (uno contro molti). Un singolo <item> diventa un oggetto; due o più diventano un array. La forma del JSON dipende da quanti fratelli c’erano in quel particolare documento. Codice consumer come result.items.item.forEach(...) funziona nei test, dove la tua fixture ha tre elementi, e solleva TypeError: not a function in produzione quando un record ne ha esattamente uno.

// Due fratelli <book> → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }

// Un solo <book> → oggetto, NON un array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }

// Normalizza in modo che entrambi i casi si comportino in modo identico
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // sicuro per 0, 1 o molti

L’idioma [].concat(x ?? []) conviene memorizzarlo: un valore mancante diventa [], un singolo oggetto diventa [object] e un array esistente passa inalterato. In Python, passa force_list={'book'} a xmltodict.parse() e il valore è sempre una lista, così la normalizzazione la salti del tutto.

2. Attributi scartati in silenzio. Diverse librerie di default ignorano gli attributi: fast-xml-parser fa proprio così finché non imposti ignoreAttributes: false. La conversione sembra riuscita, il JSON si analizza senza errori e intanto i tuoi valori id, currency e status sono spariti. Imposta sempre il flag in modo esplicito, senza fidarti del default.

3. Appiattimento dei namespace. Una dichiarazione xmlns diventa una normale chiave @_xmlns, e il prefisso in <soap:Body> sopravvive solo come parte della chiave stringa "soap:Body". La semantica — che due prefissi potrebbero legarsi allo stesso URI — si perde.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>...</soap:Body>
</soap:Envelope>
→ {
    "soap:Envelope": {
      "@_xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
      "soap:Body": "..."
    }
  }

Il prefisso soap: ora è solo testo dentro un nome di chiave; nessuno sa che è un namespace. Se due elementi di namespace diversi condividono un nome locale, possono collidere. Quando la gestione precisa dei namespace è tra i requisiti, tieni i dati in un parser namespace-aware e non appiattirli in JSON.

4. Nessuna coercizione di tipo, ed è giusto così. <zip>01234</zip> non deve diventare 1234. Codici di conto, codici postali, identificatori con padding e decimali sensibili alla precisione si rompono tutti sotto la coercizione silenziosa. Un buon convertitore mantiene tutto come stringa e ti lascia decidere tu quando convertire:

// Non affidarti alla coercizione implicita
if (config.timeout > 25) { /* fragile: "30" > 25 funziona per caso */ }

// Effettua la coercizione esplicitamente, solo dove conosci il tipo
if (parseInt(config.timeout, 10) > 25) { /* sicuro */ }

5. Perdita: commenti, processing instruction e ordine del contenuto misto. I commenti XML (<!-- ... -->) e le processing instruction (<?xml-stylesheet ?>) non hanno posto in JSON e vengono scartati. L’ordine relativo del testo intervallato con elementi figli potrebbe non resistere al round-trip. Se hai bisogno di conservare ogni byte, per riemettere il documento sorgente identico, allora non convertire: usa un Formattatore XML per riformattare o minimizzare senza toccare il modello dei dati.

Riconvertire JSON in XML (round-trip)

Andare nella direzione opposta ha il suo trucco, perché JSON non ha una regola di elemento root e XML ne pretende esattamente uno. Il Convertitore JSON in XML gemello applica le stesse convenzioni @_/#text al contrario, quindi un giro JSON → XML → JSON preserva attributi, testo e struttura.

La parte interessante è la normalizzazione della root. Il convertitore risolve il requisito di root singola con quattro regole:

  • Oggetto a chiave singola → quella chiave diventa la root: { "config": {...} }<config>...</config>.
  • Oggetto a chiavi multiple → racchiuso in <root>: { "a": 1, "b": 2 }<root><a>1</a><b>2</b></root>.
  • Array di primo livello → racchiuso come <root><item>...</item></root>, con <item> come nome di fallback fisso.
  • Valore primitivo<root>value</root>.

Tutto il resto rispecchia la direzione in avanti. Le chiavi @_ diventano attributi, #text diventa contenuto di testo, e un array JSON sotto una chiave produce fratelli ripetuti con lo stesso nome: il nome della chiave viene riutilizzato, mai messo al singolare:

// Converte JSON in XML in Node.js usando fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';

const data = {
  catalog: {
    product: {
      '@_id': 'P01',
      name: 'Wireless Headphones',
      price: { '@_currency': 'USD', '#text': '79.99' },
    },
  },
};

const builder = new XMLBuilder({
  attributeNamePrefix: '@_', // le chiavi @_ diventano attributi
  textNodeName: '#text',     // la chiave #text diventa contenuto di testo
  ignoreAttributes: false,   // elabora le chiavi @_
  format: true,              // pretty-print
});

console.log(builder.build(data));
// <catalog>
//   <product id="P01">
//     <name>Wireless Headphones</name>
//     <price currency="USD">79.99</price>
//   </product>
// </catalog>

Un dettaglio di cui si occupa il builder al posto tuo: i caratteri speciali nei valori di testo e di attributo (<, >, &, ") vengono escapati nei rispettivi riferimenti a entità, così l’output resta well-formed.

FAQ

Come vengono mappati gli attributi XML in JSON?

Gli attributi diventano chiavi con prefisso @_, quindi id="42" si trasforma in "@_id": "42". È la convenzione condivisa di fast-xml-parser e xmltodict, e il prefisso non collide mai con i nomi degli elementi perché nessun nome di elemento valido inizia con @.

Perché la conversione da XML a JSON mantiene i numeri come stringhe?

Perché il convertitore non fa alcuna coercizione di tipo. Forzare 01234 in 1234 eliminerebbe uno zero iniziale significativo da CAP, numeri di conto e ID con padding. Mantenere ogni valore come stringa è il default sicuro; la coercizione la fai a valle, di proposito, dove conosci il tipo.

La conversione da XML a JSON è senza perdite?

No. Commenti e processing instruction vengono scartati, la semantica dei namespace si conserva solo in parte e l’ordine del contenuto misto potrebbe non resistere al round-trip. Quando ti serve conservare ogni byte, usa un Formattatore XML per riformattare l’XML invece di convertirlo in JSON.

Come vengono gestiti in JSON gli elementi XML ripetuti?

Un singolo figlio con lo stesso nome diventa un oggetto; due o più diventano un array. Poiché la forma dipende dal numero di fratelli, il tuo codice consumer dovrebbe sempre normalizzare a un array così da gestire sia il caso a un elemento sia quello a molti elementi senza andare in crash.

Cosa succede ai namespace XML quando si converte in JSON?

Una dichiarazione xmlns diventa una normale chiave @_xmlns, e il prefisso resta dentro la stringa del nome dell’elemento, come in "soap:Body". Il legame semantico di un prefisso a un URI non viene interpretato, quindi namespace distinti possono appiattirsi insieme.

Come riconverto JSON in XML?

Usa il Convertitore JSON in XML gemello. Applica le stesse convenzioni @_ e #text al contrario, quindi attributi, contenuto di testo e array vengono rimappati simmetricamente. È questa simmetria a rendere possibile un round-trip JSON → XML → JSON pulito.

Posso convertire XML con elementi root multipli?

No. Più elementi di primo livello non sono XML well-formed, quindi il parser rifiuta l’input. Racchiudi prima i frammenti in un singolo elemento root (trasforma <a/><b/> in <root><a/><b/></root>) e poi converti.

Conclusione

La conversione da XML a JSON è guidata dalle convenzioni, non è una riformattazione. Le regole valgono allo stesso modo tra i runtime: gli attributi vanno su chiavi @_, il testo del contenuto misto su #text, i fratelli ripetuti su array, e i valori restano stringhe così che zeri iniziali e precisione sopravvivano. Le trappole da tenere a mente sono lo slittamento di forma singolo-contro-array, gli attributi scartati in silenzio e la perdita di commenti e di semantica dei namespace: nessuna è un bug, tutte prevedibili una volta che conosci la discrepanza dei modelli che le causa.

Quando ti serve una conversione rapida e privata, incolla nel Convertitore XML in JSON: gira interamente nel tuo browser. Valida prima la sorgente con il Formattatore XML e vai nel verso opposto con il Convertitore JSON in XML quando ti serve XML per il round-trip. Per capire meglio come i modelli dei formati di dati plasmano il comportamento della conversione, vedi le note sulle differenze tra YAML e JSON.

Articoli correlati

Vedi tutti gli articoli