Convertitore da JSON a XML
Converte in XML e mostra che cosa è stato rinominato e perché.
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 JSON qui sopra e accanto compare XML ben formato, con dichiarazione, indentazione vera e ogni carattere speciale sottoposto a escape. Se il JSON non si analizza ottieni il messaggio del parser stesso invece di un riquadro vuoto, e se una chiave ha dovuto essere rinominata per diventare un nome di elemento XML lecito, lo strumento dice quale ed in che cosa è diventata.
Ti serve quando qualcosa a valle parla solo XML: un endpoint SOAP, l’import di un ERP legacy, un formato di scambio convalidato con XSD, una fixture per un servizio che stai simulando. È la metà meno affascinante della coppia e quella con gli spigoli più vivi, perché XML impone vincoli che JSON non ha, e quei vincoli vanno risolti da qualche parte.
Tutto gira in questa scheda. Nulla viene caricato, e vale la pena dirlo: il JSON che si incolla in un convertitore è di solito una risposta di API catturata, e le risposte di API catturate contengono token, numeri di conto e schede cliente.
XML vuole esattamente una radice. JSON no.
La RFC 8259 ammette qualsiasi valore al livello superiore di un documento JSON: un oggetto, un array, una stringa, un numero, true, false o null. XML 1.0 richiede esattamente un elemento radice che contenga tutto il resto, quindi lo scarto viene risolto a ogni conversione.
Un oggetto con esattamente una chiave ha già una radice naturale, quindi quella chiave diventa l’elemento radice e non si inventa nulla. {"order": {...}} dà <order>...</order> senza involucro, ed è il caso comune perché è la forma che restituisce la conversione da XML a JSON. Tutto il resto viene avvolto, e il pannello delle note lo dice:
- Un oggetto con due o più chiavi di primo livello viene avvolto in un solo elemento, chiamato root per impostazione predefinita e modificabile nella riga dei controlli.
- Un array di primo livello viene avvolto due volte, perché un array non ha un nome di elemento proprio: ogni membro diventa <item> dentro <root>.
- Uno scalare di primo livello diventa il testo dell’elemento radice, quindi il documento JSON 42 dà <root>42</root>.
- Un null di primo livello dà un elemento radice vuoto, <root/>.
Gli array ripetono il nome dell’elemento. Non ricevono un involucro.
È la decisione che la maggior parte dei convertitori prende al contrario. {"line": ["a", "b"]} diventa due elementi <line> fratelli, non un elemento <line> con due figli <item>. La ripetizione è il modo in cui XML esprime un elenco; è la ragione per cui la direzione da XML a JSON ha il problema del singolo elemento. Inventare un involucro produce XML che nessuno schema esistente accetterebbe, e che non farebbe il viaggio di ritorno.
Ne seguono due conseguenze. Un array vuoto non produce nulla, quindi la chiave sparisce: zero ripetizioni di un elemento sono zero elementi. E un array di array si appiattisce, perché quello interno non ha un nome distinto da quello esterno: [[1,2],[3]] sotto la chiave a dà tre elementi <a>. Se una delle due cose conta, ristruttura prima il JSON.
{
"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>Le chiavi JSON spesso non sono nomi XML leciti
La sezione 2.3 di XML 1.0 definisce un Name come un NameStartChar seguito da NameChars. Un NameStartChar è una lettera, un trattino basso o due punti; non è una cifra, uno spazio, una e commerciale o un simbolo di dollaro. Una chiave JSON non ha questo vincolo, quindi "2024 total", "user@email" e "$ref" sono chiavi ordinarie e nessuna è un nome di elemento lecito.
Il mondo .NET e XSD usa l’escape, trasformando uno spazio in _x0020_, il che è esatto e illeggibile. Questo strumento invece sanifica e lo segnala: i caratteri illeciti vengono sostituiti uno a uno anziché rimossi, e un nome che continua a iniziare con una cifra riceve un prefisso. È proprio questo il punto, perché la rimozione fa collassare chiavi distinte su uno stesso nome, la sostituzione no. Ogni rinomina compare nel pannello delle note.
- "2024 total" diventa _2024_total: lo spazio viene sostituito, poi la cifra iniziale impone il prefisso.
- "2024-total" diventa _2024-total: il trattino è già lecito, quindi solo la cifra iniziale richiede il prefisso. Le due restano distinte, ed è esattamente ciò che la rimozione ti sarebbe costata.
- "user@email" diventa user_email, "$ref" diventa _ref, e una chiave vuota diventa un semplice trattino basso.
- Una chiave già lecita passa intatta, compresa una con i due punti: "soap:Body" resta "soap:Body". Ne esce un elemento con prefisso e senza dichiarazione xmlns, ben formato ma non corretto rispetto ai namespace.
Attributi, testo, e che cosa perde per primo il JSON
Le chiavi che iniziano con @_ diventano attributi dell’elemento che le racchiude, e una chiave chiamata #text fornisce il contenuto testuale. Entrambe corrispondono alla direzione da XML a JSON, quindi l’output di quella pagina si riconverte direttamente. I valori degli attributi ricevono un escape più severo del testo: oltre a &, < e ", il writer converte tabulazione, a capo e ritorno a capo in riferimenti numerici, perché la sezione 3.3.3 di XML 1.0 normalizza a spazi il bianco letterale nei valori degli attributi quando si rianalizza.
Due perdite avvengono dentro il JSON stesso, prima che questo strumento entri in gioco, e sembrano bug di conversione. I numeri JSON sono double IEEE 754, quindi un identificatore a diciannove cifre scritto come numero nudo ha già perso le cifre finali nel momento in cui il testo viene analizzato. E le chiavi duplicate le risolve il parser, con l’ultima che vince. C’è anche una stranezza di JavaScript: le chiavi che sembrano indici di array vengono enumerate per prime e in ordine numerico crescente, quindi un oggetto che mescola "2", "10" e "name" non emetterà gli elementi nell’ordine in cui li hai scritti.
null e l’oggetto vuoto producono entrambi <x/>, sono quindi indistinguibili e tornano entrambi come stringa vuota. Se ti serve la distinzione, xsi:nil="true" è l’unico modo previsto dagli standard per dire "presente ma nullo", e richiede il namespace xsi dichiarato su un antenato.
Farlo da codice
La stessa conversione nei quattro linguaggi che consumano più XML, più PHP e una riga di shell. I flag di sicurezza contano al ritorno: analizzare JSON non è il rischio, ma il codice che converte JSON in XML quasi sempre rianalizza quell’XML da qualche parte, e le impostazioni predefinite di Java e .NET risolvono un DOCTYPE se ne compare uno.
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 -Nota che cosa nessuna di queste librerie fa: sanificare una chiave in un nome XML lecito. Jackson, Json.NET e XMLBuilder o sollevano un’eccezione o emettono XML che non si analizzerà, e yq lo emette in silenzio. Se le tue chiavi JSON vengono da input utente, da un elenco di colonne di database o da una riga di intestazioni di foglio di calcolo, il passo di sanificazione tocca a te, e sostituire i caratteri invece di cancellarli è ciò che impedisce a due chiavi simili di diventare un solo elemento.
Domande frequenti
Il mio JSON esce dal browser?
No. Il parser JSON, il sanificatore dei nomi e lo scrittore XML sono tutti JavaScript eseguito in questa scheda, e non esiste alcun componente lato server con cui parlare. Apri il pannello Rete e converti qualcosa: non viene richiesto nulla.
È in questa direzione che conta di più. Il JSON incollato in un convertitore è di solito una risposta catturata da un’API in produzione durante un debug, con tanto di token di accesso o di scheda cliente completa. Diversi strumenti ben posizionati su questa ricerca inviano quel payload a un server, e uno pubblica i documenti salvati a un URL indovinabile.
Perché il mio JSON è avvolto in un elemento <root>?
Perché XML ammette esattamente un elemento radice e il tuo JSON aveva più di una chiave di primo livello, oppure era un array, oppure uno scalare nudo. Non c’è modo di scrivere due radici fratelle in XML.
Un oggetto con esattamente una chiave viene lasciato stare: quella chiave diventa la radice e non si aggiunge alcun involucro, quindi {"order": {...}} dà <order>, mentre aggiungere una seconda chiave di primo livello dà <root>. Il nome dell’involucro si modifica nella riga dei controlli. Se l’XML va dove viene convalidato, impostalo su quello che si aspetta lo schema.
Come vengono convertiti gli array JSON?
Ripetendo il nome dell’elemento una volta per membro, senza involucro. {"line": ["a", "b"]} produce due elementi <line> affiancati. È così che XML rappresenta un elenco, ed è ciò che permette all’output di fare il viaggio di ritorno. Alcuni convertitori producono invece <line><item>a</item><item>b</item></line>, che somiglia di più al JSON e fallisce la convalida contro qualunque schema scritto per XML vero.
Ne seguono due casi limite. Un array vuoto non emette nulla, quindi la chiave sparisce, e un array annidato direttamente dentro un altro si appiattisce, perché quello interno non ha un nome proprio.
Che cosa succede alle chiavi che non sono nomi di elemento validi?
Vengono rinominate, e ogni rinomina è elencata nel pannello delle note accanto all’output. I caratteri illeciti sono sostituiti uno a uno con un trattino basso, e un nome che continua a iniziare con una cifra riceve un trattino basso davanti.
Sostituire anziché rimuovere è voluto: rimuovere farebbe di "2024 total" e "2024total" lo stesso elemento, fondendo due campi distinti. Non è comunque un’iniezione perfetta: "first name" e "first_name" diventano entrambe first_name, perché nel secondo il trattino basso era già lecito.
Come ottengo attributi invece di elementi figli?
Metti il prefisso @_ alla chiave. {"user": {"@_id": "7", "name": "Alice"}} produce <user id="7"><name>Alice</name></user>. Il prefisso si modifica sopra l’editor; svuotalo e nulla verrà mai scritto come attributo.
Lì metti solo scalari. Il valore viene convertito in stringa, quindi un oggetto sotto una chiave @_ diventa l’inutile testo [object Object]. Una cosa che questo writer fa e che i più non fanno: tabulazioni, a capo e ritorni a capo dentro un valore di attributo vengono scritti come riferimenti numerici, così un valore su più righe sopravvive a una rianalisi invece di essere appiattito in spazi.
Posso riconvertire in JSON e ritrovare quello da cui sono partito?
Per la maggior parte dei documenti sì, se usi la pagina da XML a JSON con le stesse impostazioni di @_ e #text. È per questo abbinamento che quei valori predefiniti sono stati scelti.
Quattro cose non sopravvivono. null e {} diventano entrambi <x/> e tornano come stringa vuota. Un array vuoto sparisce del tutto. I tipi numerici di JSON si perdono, perché XML non ha tipi, quindi 42 torna come "42" a meno di attivare la conversione. E l’ordine delle chiavi non è significativo in nessuno dei due formati. Se il viaggio di ritorno esatto è un requisito, tieni l’XML come fonte di verità e leggilo con XPath.