Skip to content

XML ↔ JSON Converter — C++ 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 C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// xml-to-json — bidirectional XML <-> JSON converter. C++ (17) port of src/lib/xml-to-json.ts — same logic as this dir's rust.rs (Go-twin contract); JSON -> XML, prolog, and entity decoding omitted for the 80-line budget (see rust.rs / python.py).
#include <cctype>
#include <cstdio>
#include <memory>
#include <optional>
#include <string>
#include <vector>
struct El { // attributes + children in document order, direct text
    std::string tag;
    std::vector<std::pair<std::string, std::string>> attrs;
    std::vector<std::shared_ptr<El>> kids;
    std::string text;
};
struct P { const char* p; };
static void skip_ws(P& p) { while (*p.p && std::isspace((unsigned char)*p.p)) p.p++; }
static std::string parse_name(P& p) { // [A-Za-z_:][A-Za-z0-9_.:-]*
    const char* s = p.p;
    if (!(std::isalpha((unsigned char)*s) || *s == '_' || *s == ':')) return "";
    for (s++; std::isalnum((unsigned char)*s) || *s == '_' || *s == ':' || *s == '.' || *s == '-'; s++) {}
    std::string out(p.p, s); p.p = s; return out;
}
static std::string q(const std::string& s) { // JSON string literal, minimal escaping
    std::string o = "\"";
    for (char c : s) {
        if (c == '"' || c == '\\') o += '\\';
        else if (c == '\n') { o += "\\n"; continue; }
        o += (static_cast<unsigned char>(c) < 0x20) ? ' ' : c;
    }
    return o + '"';
}
// Parse one element (cursor on '<'); nullptr = malformed.
static std::shared_ptr<El> parse_element(P& p) {
    auto e = std::make_shared<El>();
    p.p++;
    e->tag = parse_name(p);
    if (e->tag.empty()) return nullptr;
    for (;;) { // attributes until '>' or '/>'
        skip_ws(p);
        if (*p.p == '/') { if (p.p[1] != '>') return nullptr; p.p += 2; return e; }
        if (*p.p == '>') { p.p++; break; }
        std::string nm = parse_name(p); skip_ws(p);
        if (nm.empty() || *p.p != '=') return nullptr;
        p.p++; skip_ws(p);
        if (*p.p != '"' && *p.p != '\'') return nullptr;
        char quote = *p.p++; const char* v = p.p;
        while (*p.p && *p.p != quote) p.p++;
        if (!*p.p) return nullptr;
        e->attrs.push_back({nm, std::string(v, p.p)});
        p.p++;
    }
    for (;;) { // content until the matching close tag
        if (!*p.p) return nullptr; // unterminated
        if (p.p[0] == '<' && p.p[1] == '/') {
            p.p += 2; std::string cn = parse_name(p); skip_ws(p);
            if (cn.empty() || *p.p != '>' || cn != e->tag) return nullptr; // mismatched
            p.p++; break;
        }
        if (p.p[0] == '<') {
            auto kid = parse_element(p);
            if (!kid) return nullptr;
            e->kids.push_back(std::move(kid));
        } else e->text += *p.p++;
    }
    return e;
}
// JSON value for an element: "@_<name>" attribute keys, bare string when
// text-only, "#text" for mixed direct text, repeated tags -> an array.
static std::string value(const El& e) {
    std::string t = e.text;
    t.erase(0, t.find_first_not_of(" \t\r\n"));
    t.erase(t.find_last_not_of(" \t\r\n") + 1);
    if (e.kids.empty() && e.attrs.empty()) return q(t); // text-only -> bare string
    std::string o = "{";
    bool first = true;
    auto kv = [&](const std::string& k, const std::string& v) {
        o += (first ? "" : ",") + q(k) + ":" + v; first = false;
    };
    for (auto& a : e.attrs) kv("@_" + a.first, q(a.second));
    std::vector<bool> done(e.kids.size(), false);
    for (size_t i = 0; i < e.kids.size(); i++) { // repeated tags -> array
        if (done[i]) continue;
        int cnt = 0;
        for (auto& k : e.kids) if (k->tag == e.kids[i]->tag) cnt++;
        if (cnt == 1) { kv(e.kids[i]->tag, value(*e.kids[i])); continue; }
        std::string a = "[";
        bool f2 = true;
        for (size_t j = 0; j < e.kids.size(); j++) if (e.kids[j]->tag == e.kids[i]->tag) {
            a += (f2 ? "" : ",") + value(*e.kids[j]);
            f2 = false; done[j] = true;
        }
        kv(e.kids[i]->tag, a + "]");
    }
    if (!t.empty()) kv("#text", q(t));
    return o + "}";
}
// Parse well-formed XML to compact JSON, or nullopt on malformation (no root,
// mismatched tags, trailing junk). Pretty-printing lives in the TS/Go twins.
static std::optional<std::string> xml_to_json(const char* xml) {
    P p{xml}; skip_ws(p);
    if (*p.p != '<') return std::nullopt;
    auto root = parse_element(p);
    if (!root) return std::nullopt;
    skip_ws(p);
    if (*p.p) return std::nullopt;
    return "{" + q(root->tag) + ":" + value(*root) + "}";
}
int main() {
    auto ok = xml_to_json("<user id=\"7\"><name>Alice</name><pet>cat</pet><pet>dog</pet></user>");
    auto bad = xml_to_json("<a><b></a>"); // mismatched close
    std::puts(ok ? ok->c_str() : "(null)");
    std::puts(bad ? bad->c_str() : "(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 →