Skip to content

JSON → TypeScript — Zig source

Paste any JSON and instantly get clean, typed TypeScript interfaces - primitives, nested objects, arrays and unions, all inferred. Optional keys, reserved-word quoting, and shape dedup are handled for you. Runs 100% in your browser.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// json-to-typescript — infer TypeScript interfaces from any JSON value.
// Zig port (canonical TS: src/lib/json-to-typescript/; Go twin:
// cli/json-to-typescript). std.json's ObjectMap preserves key order, so the
// stdlib parser feeds the condensed core directly: primitives map to TS
// primitives, arrays merge element types (objects merge keys; keys absent
// from some element become optional), and shapes dedupe by structural
// signature into named interfaces. Full port: python.py / javascript.js.
const std = @import("std");
const Allocator = std.mem.Allocator;

const Ty = struct { // every node carries its structural sig — equal shapes dedupe independent of names
    sig: []const u8,
    node: union(enum) {
        prim: []const u8,
        unk,
        arr: ?*Ty,
        uni: []*Ty,
        obj: Obj,
    },
};
const Prop = struct { key: []const u8, ty: *Ty, opt: bool };
const Obj = struct { props: []Prop, hint: []const u8 };

fn newTy(alloc: Allocator, s: []const u8, n: Ty.Node) Allocator.Error!*Ty {
    const t = try alloc.create(Ty);
    t.* = .{ .sig = s, .node = n };
    return t;
}
fn pascal(alloc: Allocator, key: []const u8) Allocator.Error![]const u8 { // `user_id` -> `UserId`; digit escapes with N
    var out = std.ArrayList(u8).init(alloc);
    var up = true;
    for (key) |c| {
        if (!std.ascii.isAlphanumeric(c)) { up = true; continue; }
        try out.append(if (up) std.ascii.toUpper(c) else c);
        up = false;
    }
    if (out.items.len == 0) try out.appendSlice("Item");
    if (std.ascii.isDigit(out.items[0])) return try std.fmt.allocPrint(alloc, "N{s}", .{out.items});
    return out.toOwnedSlice();
}
fn singular(alloc: Allocator, n: []const u8) Allocator.Error![]const u8 { // trim one trailing `s`, never `ss`
    if (n.len > 1 and n[n.len - 1] == 's' and n[n.len - 2] != 's') return n[0 .. n.len - 1];
    return std.fmt.allocPrint(alloc, "{s}Item", .{n});
}
fn objSig(alloc: Allocator, props: []const Prop) Allocator.Error![]const u8 {
    var s = std.ArrayList(u8).init(alloc);
    try s.append('{');
    for (props, 0..) |p, i| {
        if (i > 0) try s.append(';');
        try s.writer().print("{s}:{s}", .{ p.key, p.ty.sig });
    }
    try s.append('}');
    return s.toOwnedSlice();
}
fn combine(alloc: Allocator, nodes: []*Ty) Allocator.Error!*Ty { // condensed: objects merge; the rest dedupe to single-or-union
    if (nodes.len == 0) return newTy(alloc, "?", .unk);
    var objs = true;
    for (nodes) |n| if (n.node != .obj) { objs = false; };
    if (objs) return mergeObjs(alloc, nodes);
    var d = std.ArrayList(*Ty).init(alloc);
    outer: for (nodes) |n| {
        for (d.items) |u| if (std.mem.eql(u8, u.sig, n.sig)) continue :outer;
        try d.append(n);
    }
    if (d.items.len == 1) return d.items[0];
    var s = std.ArrayList(u8).init(alloc);
    try s.append('(');
    for (d.items, 0..) |m, i| {
        if (i > 0) try s.append('|');
        try s.appendSlice(m.sig);
    }
    try s.append(')');
    return newTy(alloc, try s.toOwnedSlice(), .{ .uni = try d.toOwnedSlice() });
}
fn mergeObjs(alloc: Allocator, objs: []*Ty) Allocator.Error!*Ty { // union of keys; absent key -> optional
    var keys = std.ArrayList([]const u8).init(alloc);
    outer: for (objs) |o| for (o.node.obj.props) |p| {
        for (keys.items) |k| if (std.mem.eql(u8, k, p.key)) continue :outer;
        try keys.append(p.key);
    };
    var props = std.ArrayList(Prop).init(alloc);
    for (keys.items) |k| {
        var types = std.ArrayList(*Ty).init(alloc);
        for (objs) |o| for (o.node.obj.props) |p| {
            if (std.mem.eql(u8, p.key, k)) try types.append(p.ty);
        };
        try props.append(.{ .key = k, .ty = try combine(alloc, types.items), .opt = types.items.len < objs.len });
    }
    return newTy(alloc, try objSig(alloc, props.items), .{
        .obj = .{ .props = try props.toOwnedSlice(), .hint = objs[0].node.obj.hint },
    });
}
fn infer(alloc: Allocator, v: std.json.Value, hint: []const u8) Allocator.Error!*Ty {
    switch (v) {
        .null => return newTy(alloc, "null", .{ .prim = "null" }),
        .bool => return newTy(alloc, "boolean", .{ .prim = "boolean" }),
        .integer, .float, .number_string => return newTy(alloc, "number", .{ .prim = "number" }),
        .string => return newTy(alloc, "string", .{ .prim = "string" }),
        .array => |a| {
            if (a.items.len == 0) return newTy(alloc, "[]", .{ .arr = null });
            const eh = try singular(alloc, hint);
            var el = std.ArrayList(*Ty).init(alloc);
            for (a.items) |item| try el.append(try infer(alloc, item, eh));
            const of = try combine(alloc, el.items);
            return newTy(alloc, try std.fmt.allocPrint(alloc, "[{s}]", .{of.sig}), .{ .arr = of });
        },
        .object => |o| {
            var props = std.ArrayList(Prop).init(alloc);
            var it = o.iterator();
            while (it.next()) |e| {
                const h = try std.fmt.allocPrint(alloc, "{s}{s}", .{ hint, try pascal(alloc, e.key_ptr.*) });
                try props.append(.{ .key = e.key_ptr.*, .ty = try infer(alloc, e.value_ptr.*, h), .opt = false });
            }
            return newTy(alloc, try objSig(alloc, props.items), .{
                .obj = .{ .props = try props.toOwnedSlice(), .hint = try alloc.dupe(u8, hint) },
            });
        },
    }
}
// naming (first-seen; root takes rootName; collisions get 2, 3, ...) + rendering
const Naming = struct {
    sigs: std.ArrayList([]const u8),
    names: std.ArrayList([]const u8),
    order: std.ArrayList(*Ty),
    fn nameOf(self: *Naming, sig: []const u8) ?[]const u8 {
        for (self.sigs.items, self.names.items) |s, n| if (std.mem.eql(u8, s, sig)) return n;
        return null;
    }
    fn used(self: *Naming, n: []const u8) bool {
        for (self.names.items) |x| if (std.mem.eql(u8, x, n)) return true;
        return false;
    }
};
fn collect(g: *Naming, alloc: Allocator, t: *Ty, forced: ?[]const u8) Allocator.Error!void {
    switch (t.node) {
        .obj => |o| {
            if (g.nameOf(t.sig) == null) {
                var cand = forced orelse o.hint;
                if (g.used(cand)) {
                    var i: usize = 2;
                    while (g.used(try std.fmt.allocPrint(alloc, "{s}{d}", .{ cand, i }))) i += 1;
                    cand = try std.fmt.allocPrint(alloc, "{s}{d}", .{ cand, i });
                }
                try g.sigs.append(t.sig);
                try g.names.append(cand);
                try g.order.append(t);
            }
            for (o.props) |p| try collect(g, alloc, p.ty, null);
        },
        .arr => |of| if (of) |p| try collect(g, alloc, p, null),
        .uni => |ms| for (ms) |m| try collect(g, alloc, m, null),
        else => {},
    }
}
const RESERVED = [_][]const u8{ "class", "const", "default", "function", "interface", "let", "new", "null", "return", "this", "type", "var", "boolean", "number", "string", "unknown" };
fn renderKey(alloc: Allocator, k: []const u8) Allocator.Error![]const u8 { // bareword when legal, else quoted
    var ok = k.len > 0 and (std.ascii.isAlphabetic(k[0]) or k[0] == '_' or k[0] == '$');
    for (k[1..]) |c| if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '$') { ok = false; };
    if (ok) for (RESERVED) |r| if (std.mem.eql(u8, r, k)) { ok = false; };
    return if (ok) k else std.fmt.allocPrint(alloc, "\"{s}\"", .{k});
}
fn renderType(g: *Naming, w: anytype, t: *Ty) !void {
    switch (t.node) {
        .prim => |ts| try w.print("{s}", .{ts}),
        .unk => try w.writeAll("unknown"),
        .obj => try w.print("{s}", .{g.nameOf(t.sig) orelse "unknown"}),
        .uni => |ms| for (ms, 0..) |m, i| {
            if (i > 0) try w.writeAll(" | ");
            try renderType(g, w, m);
        },
        .arr => |of| if (of) |p| {
            if (p.node == .uni) { // parens around a union element
                try w.writeAll("(");
                try renderType(g, w, p);
                try w.writeAll(")[]");
            } else {
                try renderType(g, w, p);
                try w.writeAll("[]");
            }
        } else try w.writeAll("unknown[]"),
    }
}
fn jsonToTs(alloc: Allocator, json: []const u8, rootName: []const u8) ![]const u8 {
    const parsed = try std.json.parseFromSlice(std.json.Value, alloc, json, .{});
    defer parsed.deinit();
    const root = try infer(alloc, parsed.value, try pascal(alloc, rootName));
    var g = Naming{ .sigs = std.ArrayList([]const u8).init(alloc), .names = std.ArrayList([]const u8).init(alloc), .order = std.ArrayList(*Ty).init(alloc) };
    try collect(&g, alloc, root, try pascal(alloc, rootName)); // only the actual root takes rootName
    var out = std.ArrayList(u8).init(alloc);
    const w = out.writer();
    for (g.order.items, 0..) |o, i| {
        if (i > 0) try w.writeAll("\n\n");
        try w.print("interface {s} ", .{g.nameOf(o.sig).?});
        if (o.node.obj.props.len == 0) { try w.writeAll("{}"); continue; }
        try w.writeAll("{\n");
        for (o.node.obj.props) |p| {
            try w.print("  {s}{s}: ", .{ try renderKey(alloc, p.key), if (p.opt) "?" else "" });
            try renderType(&g, w, p.ty);
            try w.writeAll(";\n");
        }
        try w.writeAll("}");
    }
    if (root.node != .obj) { // non-object roots emit a type alias
        if (g.order.items.len > 0) try w.writeAll("\n\n");
        try w.print("type {s} = ", .{try pascal(alloc, rootName)});
        try renderType(&g, w, root);
        try w.writeAll(";");
    }
    return out.toOwnedSlice();
}
pub fn main() !void { // shapes dedupe by signature; array objects merge keys (name?/active? optional)
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const out = try jsonToTs(arena.allocator(), "{\"users\":[{\"id\":1,\"name\":\"ada\"},{\"id\":2,\"active\":true}]}", "Root");
    try std.io.getStdOut().writer().print("{s}\n", .{out});
}

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 →