Skip to content

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 →