XML ↔ JSON Converter — Kotlin source
Convert XML to JSON and back, preserving attributes. Validates input and reports errors clearly, runs entirely in your browser, with a shareable link to your exact input.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// xml-to-json — bidirectional XML <-> JSON converter. Kotlin port of src/lib/xml-to-json.ts — same logic as this dir's rust.rs (Go-twin contract); JSON -> XML and entity decoding omitted for the 80-line budget (see rust.rs / python.py).
import java.io.StringReader
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.InputSource
object XmlJson {
private fun esc(s: String) = s
.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
// Element -> pretty 2-space JSON written straight to `out`: attributes
// become "@_<name>" keys, a text-only element becomes its bare string, an
// element with attributes/children becomes an object with its direct text
// under "#text", and repeated child tags collapse into an array.
private fun writeValue(el: Element, out: StringBuilder, depth: Int) {
val kids = ArrayList<Element>()
val text = StringBuilder()
var n = el.firstChild
while (n != null) {
if (n is Element) kids.add(n)
else if (n.nodeType == Node.TEXT_NODE || n.nodeType == Node.CDATA_SECTION_NODE)
text.append(n.textContent) // direct text only (skip comments/PIs)
n = n.nextSibling
}
val trimmed = text.toString().trim()
val ind = " ".repeat(depth + 1)
if (kids.isEmpty() && el.attributes.length == 0) {
out.append('"').append(esc(trimmed)).append('"') // bare string
return
}
out.append('{')
var first = true
fun key() { out.append(if (first) "\n" else ",\n").append(ind); first = false }
for (i in 0 until el.attributes.length) {
val a = el.attributes.item(i)
key(); out.append("\"@_${a.nodeName}\": \"").append(esc(a.nodeValue ?: "")).append('"')
}
for ((name, group) in kids.groupBy { it.tagName }) { // document order
key(); out.append('"').append(esc(name)).append("\": ")
if (group.size == 1) writeValue(group[0], out, depth + 1)
else { // repeated tags -> array
out.append("[\n")
group.forEachIndexed { j, k ->
if (j > 0) out.append(",\n")
out.append(" ".repeat(depth + 2)); writeValue(k, out, depth + 2)
}
out.append('\n').append(ind).append(']')
}
}
if (trimmed.isNotEmpty()) { key(); out.append("\"#text\": \"").append(esc(trimmed)).append('"') }
out.append('\n').append(" ".repeat(depth)).append('}')
}
// Parse well-formed XML to pretty JSON, or null if the DOM parser rejects
// it (mismatched tags, no root, trailing junk all throw here).
fun xmlToJson(xml: String): String? = try {
val root = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(InputSource(StringReader(xml))).documentElement
buildString {
append("{\n \"").append(esc(root.tagName)).append("\": ")
writeValue(root, this, 1)
append("\n}")
}
} catch (e: Exception) { null }
}
fun main() {
println(XmlJson.xmlToJson("<user id=\"7\"><name>Alice</name><pet>cat</pet><pet>dog</pet></user>"))
println(XmlJson.xmlToJson("<a><b></a>")) // mismatched close -> null
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →