Turning XML configuration into JSON with Python's xml.etree
Configuration files often arrive in XML because enterprise packages, network appliances and older build systems use it for years. JSON is usually easier for Python services, CI jobs and dashboards to consume. Conversion must preserve meaning rather than merely prettify the source.
Using Python's xml.etree to transform XML configuration files into JSON needs two deliberate choices: how XML maps to Python data, and which strings become JSON numbers or booleans. ElementTree handles the grammar; a conversion layer handles policy and keeps those choices testable.
That portability matters across Australia. Engineers in Sydney and Melbourne often support cloud platforms alongside on-premises systems used in Brisbane, Adelaide or Perth. When a mate asks for a “proper look” at a vendor config, portable JSON can pass through CI in an Australian cloud region or overseas. A fair dinkum setup produces the same result wherever it runs.
Define a predictable mapping
Attributes can become object members, element text can become scalar values, and child elements can become nested objects or lists. Decide those rules before writing traversal code. An explicit mapping is easier to maintain than a clever generic conversion.
<service name="orders">
<host>api.example.au</host>
<port>8443</port>
<enabled>true</enabled>
<feature>audit</feature>
<feature>metrics</feature>
</service>
{
"service": {
"name": "orders",
"host": "api.example.au",
"port": 8443,
"enabled": true,
"feature": ["audit", "metrics"]
}
}
Here, repeated feature elements form an array, while the service element becomes the top-level object member. Attributes such as name require no special case beyond string handling. ElementTree exposes them through element.attrib.
A schema should make feature an array even when only one item exists; otherwise consumers must handle both a string and a list. The same caution applies to empty containers. Missing, empty string and explicit null are different states, so define which ones the source system can produce and retain that distinction in JSON.
Parse and coerce values on purpose
A conservative converter recognises only the types required by the configuration contract. This compact example converts canonical booleans and ordinary integers, preserves decimal values as strings, and rejects duplicate singleton fields rather than silently discarding data.
import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path
def scalar(text):
text = text.strip()
if text == "true":
return True
if text == "false":
return False
if re.fullmatch(r"-?(?:0|[1-9][0-9]*)", text):
return int(text)
return text
def local_name(tag):
return tag.rsplit("}", 1)[-1]
def value_of(element, list_fields):
children = list(element)
text = (element.text or "").strip()
attributes = {
local_name(name): scalar(value)
for name, value in element.attrib.items()
}
if not children:
if text and attributes:
raise ValueError("Mixed leaf text and attributes need a schema rule")
return attributes if attributes else scalar(text)
if text:
raise ValueError("Mixed content needs a schema rule")
result = attributes
for child in children:
name = local_name(child.tag)
value = value_of(child, list_fields)
if name in list_fields:
result.setdefault(name, []).append(value)
elif name in result:
raise ValueError(f"Duplicate singleton field: {name}")
else:
result[name] = value
return result
root = ET.parse("service.xml").getroot()
document = {local_name(root.tag): value_of(root, {"feature"})}
Path("service.json").write_text(
json.dumps(document, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
The list-field set is intentionally small. A production converter should map lists by parent path, such as service/feature, so an unrelated feature deeper in the document does not inherit the same rule. It should also define behaviour for mixed content, where an element contains both text and child elements.
The integer rule deliberately leaves values such as 001, 1.2.3 and 192.168.1.10 untouched. That protects postcodes, software versions, IP addresses and identifiers from accidental conversion. Add floating-point coercion only when the source contract permits it, and decide explicitly how XML’s lack of a native null maps to JSON null.
Handle namespaces without losing identity
ElementTree represents a namespaced tag as {uri}service. Calling local_name returns service, producing clean JSON but discarding provenance; two namespaces with the same local tag can then collide.
With one known namespace, stripping it after validation is reasonable. With several, retain each URI, map it to a registered prefix, or reject collisions. Apply the same policy to namespaced attributes so the output remains internally consistent.
Group repeated elements under declared list fields and preserve document order. Test one-item, many-item and zero-item cases. When order is irrelevant, a stable sort can improve reproducibility, but it should be an explicit conversion policy.
Protect data and local conventions
Australian operations often expose dates in DD/MM/YYYY form and daylight-saving labels such as AEST and AEDT. Keep those values as strings unless a precise parser is available; turning 27/04/2026 into a number or guessing its month destroys information. For machine interchange, add a separate ISO 8601 value instead of replacing the Australian display value.
Configuration can also contain credentials, customer identifiers and audit settings. JSON does not make any of them safer. Generated files should exclude secrets, use restrictive permissions and avoid writing sensitive values into logs. Environment-variable references or a secret identifier are safer than embedded passwords, especially when the JSON will be committed, archived or sent to a managed cloud service.
Treat the XML source as untrusted when it crosses a network or upload boundary. Standard ElementTree does not resolve external entities by default, but large documents, internal entity expansion and excessive nesting can still cause trouble. Apply size and depth limits, reject a DOCTYPE declaration where policy forbids it, or parse with defusedxml. Write JSON to a temporary file and use os.replace so a failed run cannot leave a half-written configuration behind.
Package it for maintainable reuse
Give the converter a command-line interface with explicit input and output paths, a dry-run mode and a clear exit status. Automated tests should cover the sample configuration, namespaces, duplicate fields, empty elements, numeric-looking strings and malformed XML. A fixture-based test makes it obvious when a vendor changes its XML formatting or introduces a new value type.
The project can then follow the pattern used by the tools documented here: explain the purpose, show the source, state the licence, link to downloads and publish statistics where they are meaningful. Clear documentation matters because these tools appeal to developers who want to inspect and reuse the implementation rather than accept an opaque binary.
That approach suits the Australian market, where engineering teams may move workloads between on-premises infrastructure and major cloud providers while retaining the same operational controls. A compact, licensed converter built on the Python standard library fits that path without tying configuration data to one vendor. Small tools earn their keep by producing the same auditable result long after a particular appliance or release has disappeared.
