Convertitore da XML a JSON

Converte in JSON, con ogni scelta di mappatura in mano tua.

Input
Output
In attesaIncolla un documento per controllarlo. La convalida gira mentre scrivi.

Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.

Incolla XML qui sopra e il JSON compare mentre scrivi. Il documento viene prima controllato per la buona formazione, perché un convertitore che tira a indovinare dentro un markup rotto produce JSON silenziosamente sbagliato invece che palesemente sbagliato. Se la sintassi non regge, ottieni riga, colonna e rimedio invece di mezzo oggetto.

Ci si arriva quando un servizio con cui parli parla XML e tutto ciò che sta a valle parla JSON: una risposta SOAP su cui vuoi scrivere un’asserzione in un test, un feed di un fornitore che stai tirando dentro uno script, un file ONIX da esplorare prima di scrivere l’importatore. Nulla viene caricato, il che conta quando la envelope porta un token.

Qui la differenza è che la mappatura non è nascosta. Non esiste un solo modo corretto di trasformare XML in JSON: ogni convertitore prende una mezza dozzina di decisioni al posto tuo, e quasi nessuno dice quali. Questa pagina dà un nome a ciascuna decisione, ti mostra l’interruttore e riporta che cosa è costata la conversione in un pannello di note accanto all’output.

Perché non c’è una risposta giusta, solo una scelta

Il modello dati di XML è strettamente più ricco di quello di JSON. XML ha figli ordinati, attributi, testo intervallato agli elementi, namespace, commenti e CDATA. JSON ha oggetti non ordinati, array, stringhe, numeri, booleani e null. Qualunque funzione dal primo al secondo deve scartare qualcosa o inventarsi qualcosa.

Michael Kay lo ha detto senza giri di parole nel suo articolo Balisage sulla conversione consapevole dello schema: un convertitore generico "sta indovinando quale sia la semantica del modello a oggetti dietro l’XML lessicale, e sta indovinando male". Ecco i sette punti in cui deve indovinare:

  • Attributi contro elementi figli. <user id="7"/> e <user><id>7</id></user> sono documenti diversi che quasi tutti vogliono come lo stesso JSON. Uniscili e <user id="7"><id>8</id></user> produce una chiave duplicata, che la RFC 8259 definisce imprevedibile.
  • Una occorrenza o molte. Nulla in <items><item>a</item></items> dice se item può ripetersi, quindi un convertitore indovina contando.
  • Contenuto misto. <p>Del testo <b>importante</b>.</p> ha tre figli ordinati, e un oggetto JSON non può esprimere quell’ordine.
  • Testo di soli spazi. L’a capo e l’indentazione fra elementi in un XML formattato sono veri nodi di testo, e tenerli è fedele ma inutilizzabile.
  • Namespace. Un prefisso non è il nome: il nome è l’URI, e JSON non ha alcun concetto di namespace.
  • Commenti e istruzioni di elaborazione. Nessuno dei due esiste in JSON.
  • Elementi vuoti. <e/> si mappa in modo plausibile su null, "", {} o una chiave di testo vuota, e <e/> ed <e></e> sono lo stesso documento.

La convenzione usata qui, e gli interruttori che la cambiano

Il valore predefinito è la convenzione del prefisso di attributo descritta da Stefan Goessner nel 2006, di cui fast-xml-parser, xml2js e l’SDK di AWS usano varianti. Gli attributi prendono il prefisso @_ così non possono collidere con un elemento figlio dello stesso nome. Il testo che condivide un elemento con attributi o figli finisce sotto #text. Un elemento che compare una volta è un valore; se compare due volte diventa un array. Un elemento di solo testo collassa in una stringa semplice, quindi <name>Alice</name> è "Alice".

L’elemento radice resta la chiave più esterna, i commenti vengono eliminati, gli spazi vengono tagliati e i riferimenti a entità risolti, quindi un attributo scritto "A &amp; B" arriva come "A & B". I controlli sopra l’editor impostano il prefisso degli attributi, la chiave del testo, un elenco "sempre un array" di nomi di elemento e tre caselle: rimuovi i prefissi di namespace, scarta gli attributi, converti i tipi. Tutte e tre sono disattivate.

<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": ""
  }
}
Impostazioni predefinite, su un documento che contiene tutti e quattro i casi scomodi.

Perché la conversione dei tipi è disattivata

XML senza schema è testo fino in fondo. Trasformare "123" in un numero è comodo esattamente fino al punto in cui distrugge un identificatore, e gli identificatori sono gran parte di ciò che passa nelle integrazioni XML.

Quando attivi la conversione, questa è volutamente stretta. Un valore diventa numero solo se corrisponde a una grammatica JSON dei numeri rigorosa e poi sopravvive a un’andata e ritorno: il risultato analizzato viene riserializzato e confrontato con l’originale, carattere per carattere. È quel controllo a evitare i guasti con cui altri convertitori escono. Anche con la conversione attiva, questi restano stringhe:

  • Zeri iniziali. "00042" e "01730" falliscono subito la grammatica, perché un numero JSON non può iniziare con uno zero seguito da altre cifre. CAP, codici bancari e codici articolo sopravvivono.
  • Zeri finali dopo la virgola. "19.90" viene letto come 19.9, che si riserializza in "19.9", quindi la stringa resta. "1.10" non diventa 1.1.
  • Interi che un double non regge. "9007199254740993" diventa un valore che finisce per 992, l’andata e ritorno fallisce e la stringa resta. È questo che altrove corrompe identificatori d’ordine a diciannove cifre.
  • Esponenti non canonici. "1e5" viene letto come 100000, che non è "1e5", quindi resta testo.
  • Tutto ciò che non è esattamente true, false o null. "TRUE", "yes" e "Y" restano stringhe.

Che cosa si perde, e le alternative che hanno un nome

Tre cose non sopravvivono. L’ordine del documento fra fratelli con nomi diversi sparisce, quindi se <line> e <discount> si alternano il JSON non dice nulla su come. Il contenuto misto viene appiattito: i frammenti di testo vengono concatenati e gli elementi in mezzo migrano in chiavi proprie, così <root>35<nested>34</nested>46</root> dà un valore testuale "3546". I commenti vengono scartati.

I namespace sono mantenuti alla lettera invece che risolti, quindi soap:Body diventa la chiave "soap:Body". Togliere i prefissi dà "Body", ma allora due elementi di namespace diversi con lo stesso nome locale collidono su un’unica chiave, e JSON non offre una terza via.

Se questa convenzione non è quella che il tuo consumatore si aspetta, le alternative hanno un nome. BadgerFish mette il testo sotto $, gli attributi sotto @nome e porta con sé ogni namespace in ambito: ottimo per l’andata e ritorno, quasi illeggibile. Parker scarta gli attributi e assorbe la radice, l’output più asciutto e più a senso unico di tutti. JsonML scrive ogni elemento come [nome, attributi, figli], l’unica convenzione diffusa che mantiene intatti contenuto misto e ordine dei figli.

Farlo da codice

La stessa conversione nei linguaggi che l’XML lo consumano davvero. Ogni esempio disattiva la risoluzione delle entità, perché in parecchi di questi stack il comportamento predefinito scarica una URL indicata in un DOCTYPE, che è la vulnerabilità XXE. Ognuno indica anche la decisione di mappatura che quella libreria prende al posto tuo.

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)'

Ogni libreria qui sopra prende almeno una decisione di mappatura in silenzio. Jackson fonde gli attributi con i figli, SimpleXML scarta il testo del contenuto misto, Json.NET e yq non possono sapere quali elementi siano liste. È l’ambiguità che affiora, non un bug di nessuna di esse. Qualunque tu scelga, scrivi un test che faccia passare una risposta con un solo elemento e una con più elementi per lo stesso percorso di codice.

Domande frequenti

Il mio XML viene inviato a un server?

No. Lo scanner, il mappatore e il serializzatore JSON sono tutti JavaScript che gira in questa scheda, e non esiste alcun backend che la conversione possa raggiungere.

Apri gli strumenti per sviluppatori, passa alla scheda Rete, poi incolla e guarda: le risorse di questa pagina si caricano una volta e non segue nient’altro. Qui conta più che nella maggior parte dei convertitori, perché l’XML che si converte è di solito un payload di integrazione, con tanto di header WS-Security o chiave API.

Perché un solo <item> mi dà un oggetto e due mi danno un array?

Perché senza schema XML non ha cardinalità. Nulla nel documento dice se item possa ripetersi, quindi un convertitore indovina contando, e così la forma del tuo JSON dipende dai dati invece che dal contratto. È il modo più comune in cui si rompe un’integrazione XML: codice scritto su una risposta di prova con tre elementi chiama items.item.map() e funziona finché non arriva un cliente con un solo ordine.

Il rimedio è il campo "sempre un array" sopra l’editor. Fai lo stesso nel codice: fast-xml-parser ha isArray, xmltodict ha force_list, e xml2js imposta explicitArray a true per default proprio per questo.

Come mantengo gli attributi XML convertendo in JSON?

Sono mantenuti per impostazione predefinita, sotto chiavi con prefisso @_, quindi <user id="7"/> diventa {"user": {"@_id": "7"}}. Il prefisso esiste perché un attributo e un elemento figlio con lo stesso nome non possano sovrascriversi.

Puoi cambiare il prefisso o svuotarlo per fondere gli attributi fra i figli. L’output fuso si legge meglio e perde in silenzio un valore quando i nomi collidono, come in <user id="7"><id>8</id></user>. Per scartare del tutto gli attributi c’è una casella, che è ciò che fa la convenzione Parker: va bene per un’estrazione a senso unico, è sbagliata per qualunque cosa tu debba riconvertire.

Che cosa succede a namespace e prefissi come soap:?

Il nome con prefisso viene usato alla lettera, quindi soap:Body diventa la chiave "soap:Body". Nulla viene risolto, perché JSON non può portare una URI di namespace, e il pannello delle note dice quanti namespace erano dichiarati.

Spuntare "rimuovi i prefissi di namespace" dà "Body", di solito quello che vuoi quando estrai un valore da una risposta SOAP. Il rischio è concreto: se il documento ha sia soap:Header sia wsse:Header, la rimozione li fonde su un’unica chiave e uno dei due vince.

Conviene attivare "converti numeri e booleani"?

Solo se sai che nei tuoi dati non ci sono identificatori. La conversione qui è più rigorosa della media: richiede una grammatica numerica stretta e poi un controllo di andata e ritorno, così "00042", "19.90", "1.10" e qualunque intero troppo grande per un double restano stringhe invece di essere rovinati.

Quello che non può coprire è un campo che vale "1" nel tuo campione e "N/A" martedì prossimo. Una via di mezzo ragionevole è lasciarla spenta e convertire nel tuo codice i due o tre campi che ti servono davvero, dove la decisione resta scritta.

Che cosa fa con commenti, CDATA ed elementi vuoti?

Commenti e istruzioni di elaborazione vengono eliminati. JSON non ha dove metterli, e inventare una chiave rende l’output più scomodo da consumare per del contenuto che per definizione non è dato.

Il CDATA è trattato come testo: <![CDATA[a < b]]> e a &lt; b sono lo stesso contenuto scritto in due modi. Un elemento vuoto diventa la stringa vuota, quindi <note/> ed <note></note> danno entrambi "note": "". null è stato scartato perché si legge come "valore ignoto" invece che "presente e vuoto".

Strumenti correlati

Approfondimenti