XML ↔ JSON Converter — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// xml-to-json — bidirectional XML <-> JSON converter. Zig (0.13) 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).
const std = @import("std");
const Value = std.json.Value;
const Kid = struct { tag: []const u8, value: Value };
const P = struct {
s: []const u8,
pos: usize = 0,
fn peek(self: *P) u8 { return if (self.pos < self.s.len) self.s[self.pos] else 0; }
fn starts(self: *P, pre: []const u8) bool { return std.mem.startsWith(u8, self.s[self.pos..], pre); }
fn ws(self: *P) void { while (self.pos < self.s.len and std.ascii.isWhitespace(self.s[self.pos])) self.pos += 1; }
};
fn nameChar(c: u8) bool {
return std.ascii.isAlphanumeric(c) or c == '_' or c == ':' or c == '.' or c == '-';
}
fn parseName(p: *P) ![]const u8 { // [A-Za-z_:][A-Za-z0-9_.:-]*
const c = p.peek();
if (!(std.ascii.isAlphabetic(c) or c == '_' or c == ':')) return error.Malformed;
const start = p.pos;
p.pos += 1;
while (nameChar(p.peek())) p.pos += 1;
return p.s[start..p.pos];
}
// Parse one element (cursor on '<'): "@_<name>" attribute keys, bare string
// when text-only, object with "#text" for mixed content, repeated tags -> an
// array — the Go-twin contract. error.Malformed = bad input.
fn parseElement(p: *P, arena: std.mem.Allocator) !Kid {
p.pos += 1; // "<"
const tag = try parseName(p);
var map = std.json.ObjectMap.init(arena); // attrs as parsed, then children
var kids = std.ArrayList(Kid).init(arena);
var text = std.ArrayList(u8).init(arena);
var self_close = false;
while (true) { // attributes until '>' or '/>'
p.ws();
if (p.peek() == '>') { p.pos += 1; break; }
if (p.starts("/>")) { p.pos += 2; self_close = true; break; }
const an = try parseName(p);
p.ws();
if (p.peek() != '=') return error.Malformed;
p.pos += 1;
p.ws();
const qu = p.peek();
if (qu != '"' and qu != '\'') return error.Malformed;
p.pos += 1;
const vs = p.pos;
while (p.pos < p.s.len and p.s[p.pos] != qu) p.pos += 1;
if (p.pos >= p.s.len) return error.Malformed;
const key = try std.fmt.allocPrint(arena, "@_{s}", .{an});
try map.put(key, .{ .string = p.s[vs..p.pos] });
p.pos += 1;
}
if (!self_close) {
while (!p.starts("</")) { // content until the matching close tag
if (p.pos >= p.s.len) return error.Malformed; // unterminated
if (p.peek() != '<') { // character data until the next markup byte
const ts = p.pos;
while (p.pos < p.s.len and p.s[p.pos] != '<') p.pos += 1;
try text.appendSlice(p.s[ts..p.pos]);
} else try kids.append(try parseElement(p, arena));
}
p.pos += 2; // "</"
const cn = try parseName(p);
p.ws();
if (p.peek() != '>' or !std.mem.eql(u8, cn, tag)) return error.Malformed; // mismatched
p.pos += 1;
}
const t = std.mem.trim(u8, text.items, " \t\r\n");
if (map.count() == 0 and kids.items.len == 0)
return Kid{ .tag = tag, .value = .{ .string = t } }; // text-only -> bare string
for (kids.items) |k| { // repeated tags -> array (first-occurrence order)
if (map.getPtr(k.tag)) |ex| {
switch (ex.*) {
.array => |*arr| try arr.append(arena, k.value),
else => {
var arr = std.json.Array.init(arena);
try arr.append(arena, ex.*);
try arr.append(arena, k.value);
ex.* = .{ .array = arr };
},
}
} else try map.put(k.tag, k.value);
}
if (t.len > 0) try map.put("#text", .{ .string = t });
return Kid{ .tag = tag, .value = .{ .object = map } };
}
// Parse well-formed XML to pretty 2-space JSON, or error.Malformed (no root,
// mismatched tags, trailing junk). ObjectMap keeps document order, like TS.
pub fn xmlToJson(arena: std.mem.Allocator, xml: []const u8) ![]const u8 {
var p = P{ .s = xml };
p.ws();
if (p.peek() != '<') return error.Malformed; // no root element
const root = try parseElement(&p, arena);
p.ws();
if (p.pos != p.s.len) return error.Malformed; // trailing junk
var top = std.json.ObjectMap.init(arena);
try top.put(root.tag, root.value);
return std.json.stringifyAlloc(arena, Value{ .object = top }, .{ .whitespace = .indent_2 });
}
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 →