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. Language: C (C11, stdlib only). Port of src/lib/xml-to-json.ts — same logic as this dir's rust.rs (Go-twin contract); JSON -> XML, prolog, comments, and entity decoding omitted for the 80-line budget (see rust.rs / python.py). */
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
typedef struct { const char *p; } P;
static char pool[1 << 20]; static size_t pool_off; /* bump arena (demo lifetime) */
static char *dupn(const char *s, size_t n) {
    char *d = pool + pool_off; memcpy(d, s, n); d[n] = 0; pool_off += n + 1; return d;
}
static void skip_ws(P *p) { while (*p->p && isspace((unsigned char)*p->p)) p->p++; }
static char *parse_name(P *p) { /* [A-Za-z_:][A-Za-z0-9_.:-]* */
    const char *s = p->p;
    if (!(isalpha((unsigned char)*s) || *s == '_' || *s == ':')) return NULL;
    for (s++; isalnum((unsigned char)*s) || *s == '_' || *s == ':' || *s == '.' || *s == '-'; s++) {}
    char *out = dupn(p->p, (size_t)(s - p->p)); p->p = s; return out;
}
static char *q(const char *s) { /* JSON string literal, minimal escaping */
    char b[8192]; size_t o = 0; b[o++] = '"';
    for (; *s && o < sizeof b - 3; s++) {
        if (*s == '"' || *s == '\\') b[o++] = '\\';
        b[o++] = (unsigned char)*s < 0x20 ? ' ' : *s;
    }
    b[o++] = '"';
    return dupn(b, o);
}
/* Compose helper: printf-style append into scratch. Static state is safe —
   compose phases never nest across recursion levels. */
static char o_[65536]; static size_t ol;
static void app(const char *fmt, ...) {
    va_list ap;
    if (ol >= sizeof o_ - 1) return;
    va_start(ap, fmt);
    ol += (size_t)vsnprintf(o_ + ol, sizeof o_ - ol, fmt, ap);
    va_end(ap);
}
/* Parse one element (cursor on '<'), return its JSON value, set *tag: "@_<name>"
   attribute keys, bare string when text-only, object with "#text" for mixed
   content, repeated tags -> an array (the Go-twin contract). */
static char *parse_element(P *p, char **tag) {
    char *an[32], *av[32], *kt[64], *kv[64]; int na = 0, nk = 0;
    char text[8192] = "", *tp = text; size_t tlen = 0;
    p->p++;
    if (!(*tag = parse_name(p))) return NULL;
    for (;;) { /* attributes until '>' or '/>' */
        skip_ws(p);
        if (*p->p == '/') { if (p->p[1] != '>') return NULL; p->p += 2; goto compose; }
        if (*p->p == '>') { p->p++; break; }
        char *nm = parse_name(p); skip_ws(p);
        if (!nm || *p->p != '=' || na >= 32) return NULL;
        p->p++; skip_ws(p);
        if (*p->p != '"' && *p->p != '\'') return NULL;
        char qu = *p->p++; const char *v = p->p;
        while (*p->p && *p->p != qu) p->p++; if (!*p->p) return NULL;
        an[na] = nm; av[na++] = dupn(v, (size_t)(p->p - v)); p->p++;
    }
    for (;;) { /* content until the matching close tag */
        if (!*p->p) return NULL; /* unterminated */
        if (p->p[0] == '<' && p->p[1] == '/') {
            p->p += 2; char *cn = parse_name(p); skip_ws(p);
            if (!cn || *p->p != '>' || strcmp(cn, *tag)) return NULL; /* mismatched */
            p->p++; break;
        }
        if (p->p[0] == '<') { /* child element */
            char *ct, *cv;
            if (!(cv = parse_element(p, &ct)) || nk >= 64) return NULL;
            kt[nk] = ct; kv[nk++] = cv;
        } else if (tlen < sizeof text - 1) text[tlen++] = *p->p++;
        else return NULL;
    }
compose:
    while (tlen && isspace((unsigned char)*tp)) tp++, tlen--;
    while (tlen && isspace((unsigned char)tp[tlen - 1])) tlen--;
    tp[tlen] = '\0'; /* NUL-terminate at the trimmed length for q() */
    if (!na && !nk) return q(tp); /* text-only -> bare string */
    ol = 0; int nout = 0, done[64] = {0};
    app("{");
    for (int i = 0; i < na; i++) app("%s\"@_%s\":%s", nout++ ? "," : "", an[i], q(av[i]));
    for (int i = 0; i < nk; i++) { /* repeated tags -> array, first-occurrence order */
        if (done[i]) continue;
        int cnt = 0; for (int j = 0; j < nk; j++) if (!strcmp(kt[j], kt[i])) cnt++;
        if (cnt == 1) { app("%s%s:%s", nout++ ? "," : "", q(kt[i]), kv[i]); continue; }
        app("%s%s:[", nout++ ? "," : "", q(kt[i]));
        for (int j = 0, s = 0; j < nk; j++) if (!strcmp(kt[j], kt[i])) { app("%s%s", s++ ? "," : "", kv[j]); done[j] = 1; }
        app("]");
    }
    if (tlen) app(",\"#text\":%s", q(tp));
    app("}");
    return dupn(o_, ol);
}
/* Parse well-formed XML to compact JSON, or NULL on malformation (no root,
   mismatched tags, trailing junk). Pretty-printing lives in the TS/Go twins. */
char *xml_to_json(const char *xml) {
    P p = { xml }; skip_ws(&p);
    if (*p.p != '<') return NULL; /* no root element */
    char *tag, *v = parse_element(&p, &tag);
    if (!v || (skip_ws(&p), *p.p)) return NULL; /* parse failure or trailing junk */
    char out[65536]; int n = snprintf(out, sizeof out, "{%s:%s}", q(tag), v);
    return n > 0 && (size_t)n < sizeof out ? dupn(out, (size_t)n) : NULL;
}
int main(void) {
    char *ok = xml_to_json("<user id=\"7\"><name>Alice</name><pet>cat</pet><pet>dog</pet></user>"), *bad = xml_to_json("<a><b></a>");
    puts(ok ? ok : "(null)"); puts(bad ? bad : "(null)"); /* mismatched close -> (null) */
    return 0;
}

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 →