Skip to content

JSON to Zod Schema — Zig source

Generate Zod validation schemas from JSON. Infers z.string, z.number, z.boolean, z.object, z.array, z.null, and z.union for mixed arrays.

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

//! JSON -> Zod schema inference — Zig port of the json-to-zod tool: parse
//! with std.json (its ObjectMap is an array hash map, so textual key order
//! survives) and run the recursive InferZod walk from jsonToZod.ts / go.go.
//! Mixed arrays render z.union([every element]); one shared type z.array(T).
const std = @import("std");
const Allocator = std.mem.Allocator;

fn pad(alloc: Allocator, n: usize) ![]const u8 {
    const s = try alloc.alloc(u8, n);
    @memset(s, ' ');
    return s;
}

fn infer(alloc: Allocator, value: std.json.Value, indent: usize) Allocator.Error![]u8 {
    var out = std.ArrayList(u8).init(alloc);
    errdefer out.deinit();
    switch (value) {
        .null => try out.appendSlice("z.null()"),
        .bool => try out.appendSlice("z.boolean()"),
        .integer, .float, .number_string => try out.appendSlice("z.number()"),
        .string => try out.appendSlice("z.string()"),
        .array => |items| {
            if (items.items.len == 0) {
                try out.appendSlice("z.array(z.unknown())");
                return out.toOwnedSlice();
            }
            var types = std.ArrayList([]const u8).init(alloc);
            defer types.deinit();
            for (items.items) |e| try types.append(try infer(alloc, e, indent + 2));
            var same = true; // one shared element type <=> all equal the first
            for (types.items[1..]) |t| { if (!std.mem.eql(u8, t, types.items[0])) { same = false; break; } }
            if (same) {
                try out.writer().print("z.array({s})", .{types.items[0]});
            } else { // union of EVERY element (duplicates kept)
                var joined = std.ArrayList(u8).init(alloc);
                defer joined.deinit();
                for (types.items, 0..) |t, i| {
                    try joined.appendSlice(t);
                    try joined.appendSlice(if (i + 1 < types.items.len) ",\n" else "");
                }
                try out.appendSlice("z.array(z.union([\n");
                var lines = std.mem.splitScalar(u8, joined.items, '\n');
                while (lines.next()) |line| { // pad EVERY line, like TS pad()
                    if (line.len > 0) try out.appendSlice(try pad(alloc, indent + 2));
                    try out.appendSlice(line);
                    try out.appendSlice("\n");
                }
                try out.appendSlice("]))"); // close unpadded: pad("", indent) is ""
            }
        },
        .object => |map| {
            if (map.count() == 0) {
                try out.appendSlice("z.object({})");
                return out.toOwnedSlice();
            }
            try out.appendSlice("z.object({\n"); // fields in textual order
            var it = map.iterator();
            while (it.next()) |entry| {
                try out.appendSlice(try pad(alloc, indent + 2));
                try out.writer().print("{s}: ", .{entry.key_ptr.*});
                try out.appendSlice(try infer(alloc, entry.value_ptr.*, indent + 2));
                try out.appendSlice(",\n");
            }
            try out.appendSlice(try pad(alloc, indent));
            try out.appendSlice("})");
        },
    }
    return out.toOwnedSlice();
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    const parsed = try std.json.parseFromSlice(std.json.Value, alloc,
        \\{"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}
    , .{});
    const schema = try infer(alloc, parsed.value, 2);
    std.debug.print("const User = {s};\n", .{schema});
}

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 →