Skip to content

JSON Minify — Zig source

Remove all whitespace from JSON for the most compact serialization. Validates as it minifies, 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.

//! json-minify — compact JSON to its smallest valid form. Zig port (canonical
//! TS: src/lib/json-minify.ts; Go twin: cli/json-minify). No stdlib JSON
const std = @import("std");
const Scan = struct {
    src: []const u8, out: []u8, i: usize = 0, o: usize = 0,
    fn peek(s: *Scan) u8 { return if (s.i < s.src.len) s.src[s.i] else 0; }
    fn take(s: *Scan) u8 { const c = s.peek(); s.i += 1; return c; } // 0 at end-of-input
    fn skipWs(s: *Scan) void { while (switch (s.peek()) { ' ', '\t', '\n', '\r' => true, else => false }) s.i += 1; }
    fn put(s: *Scan, c: u8) void { s.out[s.o] = c; s.o += 1; }
    fn digits(s: *Scan) bool { var any = false; while (s.peek() >= '0' and s.peek() <= '9') { s.i += 1; any = true; } return any; }
    fn string(s: *Scan) bool {
        s.put('"'); s.i += 1; // opening quote
        while (true) {
            const c = s.take();
            if (c == '"') { s.put('"'); return true; }
            if (c == '\\') {
                const e = s.take(); s.put(e);
                if (e == 'u') {
                    for (0..4) |_| { const h = s.take(); if (!isHex(h)) return false; s.put(h); }
                } else if (std.mem.indexOfScalar(u8, "\"\\/bfnrt", e) == null) return false; // 0 is not in the set
            } else if (c < 0x20) return false // control byte / EOF
            else s.put(c);
        }
    }
    fn isHex(h: u8) bool { return (h >= '0' and h <= '9') or (h >= 'a' and h <= 'f') or (h >= 'A' and h <= 'F'); }
    fn number(s: *Scan) bool {
        const start = s.i;
        if (s.peek() == '-') s.i += 1;
        if (s.peek() == '0') s.i += 1 // "01" is rejected by the caller
        else if (s.peek() >= '1' and s.peek() <= '9') { s.i += 1; while (s.peek() >= '0' and s.peek() <= '9') s.i += 1; }
        else return false; // bare '-'
        if (s.peek() == '.') { s.i += 1; if (!s.digits()) return false; }
        if (s.peek() == 'e' or s.peek() == 'E') { s.i += 1; if (s.peek() == '+' or s.peek() == '-') s.i += 1; if (!s.digits()) return false; }
        const n = s.i - start;
        @memcpy(s.out[s.o .. s.o + n], s.src[start..s.i]); s.o += n;
        return true;
    }
    fn literal(s: *Scan, w: []const u8) bool {
        if (s.i > s.src.len or s.src.len - s.i < w.len) return false;
        if (!std.mem.eql(u8, s.src[s.i .. s.i + w.len], w)) return false;
        @memcpy(s.out[s.o .. s.o + w.len], w); s.i += w.len; s.o += w.len;
        return true;
    }
    fn value(s: *Scan) bool {
        s.skipWs();
        const c = s.peek();
        if (c == '{' or c == '[') {
            const close: u8 = if (c == '{') '}' else ']';
            s.put(c); s.i += 1; s.skipWs();
            if (s.peek() == close) { s.put(close); s.i += 1; return true; } // {} / []
            while (true) {
                if (close == '}') { s.skipWs(); if (s.peek() != '"' or !s.string()) return false; s.skipWs(); if (s.peek() != ':') return false; s.i += 1; s.put(':'); }
                if (!s.value()) return false;
                s.skipWs();
                const d = s.take();
                if (d == ',') { s.put(d); continue; }
                if (d == close) { s.put(close); return true; }
                return false;
            }
        }
        if (c == '"') return s.string();
        if (c == 't' or c == 'f' or c == 'n') return s.literal(if (c == 't') "true" else if (c == 'f') "false" else "null");
        return (c == '-' or (c >= '0' and c <= '9')) and s.number();
    }
};
pub fn minify(alloc: std.mem.Allocator, input: []const u8) ?[]u8 {
    const buf = alloc.alloc(u8, input.len) catch return null;
    var s = Scan{ .src = input, .out = buf };
    if (!s.value()) { alloc.free(buf); return null; }
    s.skipWs();
    if (s.i != s.src.len) { alloc.free(buf); return null; } // trailing garbage
    return buf[0..s.o];
}
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const min = minify(arena.allocator(), "{\n  \"a\": 1,\n  \"b\": [\n    1,\n    2\n  ]\n}") orelse return error.Invalid;
    std.debug.print("{s}\n", .{min}); // {"a":1,"b":[1,2]}
    if (minify(arena.allocator(), "{invalid}") != null) return error.Invalid; // invalid JSON -> 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 →