How to convert JSON to XML in PHP with attributes, CDATA, and repeated elements

- Pubblicato in coding di - Permalink

Converting a flat JSON object like {"nome": "Mario", "eta": 34} to XML is a five-line recursive function — we covered that ground in the first article of this series. The moment real data shows up, three features do all the heavy lifting: attributes, CDATA, and repeated elements. This is the reference for all three, using a35g/json-to-xml:

composer require a35g/json-to-xml
use A35G\JsonToXml\JsonToXmlConverter;

$converter = new JsonToXmlConverter(rootName: 'root', itemNodeName: 'item', useCdata: true);

Every example below assumes $converter is set up like this unless noted otherwise.

Attributes

Prefix a key with @ and it becomes an XML attribute on the current node instead of a child element:

$converter->jsonToXmlString('{
    "request": {"@code": "", "@typeReq": "ABC", "cliente": "Mario Rossi"}
}');
<root>
  <request code="" typeReq="ABC">
    <cliente>Mario Rossi</cliente>
  </request>
</root>

Two things worth knowing:

  • Attributes always render first, regardless of where the @key sits among your JSON keys. The library does two passes over each object — attributes, then everything else — so you don't need to order your JSON carefully to get attributes into the opening tag.
  • An attribute can't hold an array. {"@attr": {"non": "valido"}} throws an InvalidArgumentException instead of silently producing broken XML. If you need structured data on a node, that's what a child element is for.

Mixed content: attributes plus direct text

Sometimes a node needs both — an attribute and text content, no child elements. The #text key covers that:

$converter->jsonToXmlString('{
    "operatore": {"@codArea": "XXX", "#text": "contiene <tag>"}
}');
<operatore codArea="XXX"><![CDATA[contiene <tag>]]></operatore>

Notice the text automatically went into CDATA — which brings us to the next feature.

Automatic CDATA

CDATA is applied only when a value's content actually needs it: when it contains <, >, &, a quote character, or a newline.

$converter->jsonToXmlString('{"partitaIva": "12345678901"}');
// <partitaIva>12345678901</partitaIva>          — no CDATA, doesn't need it

$converter->jsonToXmlString('{"descrizione": "Testo con <tag> speciali"}');
// <descrizione><![CDATA[Testo con <tag> speciali]]></descrizione>

There's no JSON key to force CDATA independently of the content — it's always content-driven. If you'd rather never use CDATA and rely on standard XML escaping instead, turn it off at construction time:

$converter = new JsonToXmlConverter(rootName: 'root', useCdata: false);

$converter->jsonToXmlString('{"testo": "ciao <mondo> & amici"}');
// <testo>ciao <mondo> & amici</testo>

This is a per-converter setting, not per-call — decide once, based on what your downstream XML consumer expects.

Repeated elements, without a wrapper

Any JSON array becomes the same element repeated once per item — never an intermediate wrapper element:

$converter->jsonToXmlString('{
    "Nota": [{"Principale": "0"}, {"Principale": "1"}]
}');
<root>
  <Nota><Principale>0</Principale></Nota>
  <Nota><Principale>1</Principale></Nota>
</root>

Not <Nota><item>...</item><item>...</item></Nota> — just two sibling <Nota> elements, which is what most real-world XML schemas actually expect for repeated data.

A few edge cases that matter in practice:

  • Arrays of scalars work the same way: {"item": ["one", "two"]} becomes <item>one</item><item>two</item>.
  • Empty arrays produce nothing at all. {"tags": []} doesn't create an empty <tags> element or any trace of the key — it's treated as "zero repetitions," which is usually exactly what you want.
  • itemNodeName is your fallback for numeric-keyed data. If your data has a JSON object with non-sequential numeric-like keys (unusual, but it happens with some data sources), the library falls back to itemNodeName — the second constructor argument, item by default — for those child names, since there's no meaningful key to use as a tag name.

Null, booleans, and everything else scalar

Rounding out the reference:

$converter->jsonToXmlString('{"note": null, "attivo": true, "eliminato": false}');
<root>
  <note/>
  <attivo>true</attivo>
  <eliminato>false</eliminato>
</root>

null produces an empty self-closing element — not an omitted key, not the string "null". Booleans are stringified as true / false, not 1 / "" (PHP's usual scalar coercion for booleans, which would produce confusing XML).

Tag name sanitization

JSON keys don't have to be valid XML names, so every key is sanitized before it becomes a tag or attribute name: disallowed characters become _, a name can't start with a digit, dot, or hyphen (a leading n_ is added), and a name can't start with the reserved xml prefix (a leading _ is added instead). A key like "1campo con spazi" becomes the tag n_1campo_con_spazi — unusual input still produces valid XML instead of throwing partway through generation.

Two smaller things worth knowing

Unicode just works — {"testo": "Città € 日本語 🚀"} round-trips through the XML output unchanged, since the document is UTF-8 throughout.

Writing to a file or straight to output is one call each, and both clean up after themselves — jsonToXmlFile() writes directly, and jsonToXmlStdOut() (handy for streaming an XML HTTP response) uses a temporary file internally that's always deleted, even if the JSON turns out to be invalid partway through.

Takeaway

Attributes, CDATA, and repeated elements are the three features that turn "converts an object" into "converts real JSON into XML a strict schema will actually accept." Between this article and the XML→JSON side covered previously, that's the whole format covered — from here, the live demo is the fastest way to try your own JSON against it.

→