Skip to content

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 →