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 →