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 →