Skip to content

JSON to Zod Schema — C 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 C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/* JSON -> Zod schema inference — C (C11) port of the json-to-zod tool: the
 * recursive InferZod walk from jsonToZod.ts / go.go; no JSON engine, so the
 * value tree is a hand-built tagged union the Go twin's decoder fills. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ };
typedef struct JVal {
    int kind, n;              /* n: element / field count                     */
    double num; const char *str;               /* J_NUM / J_STR              */
    struct JVal *items; const char **keys; /* J_ARR/J_OBJ: keys[i]->items[i] */
} JVal;
typedef struct { char *s; size_t n, cap; } Buf; /* growable output string */
static void put(Buf *b, char c) {
    if (b->n == b->cap) { b->cap = b->cap ? b->cap * 2 : 64; b->s = realloc(b->s, b->cap); }
    b->s[b->n++] = c;
}
static void puts_(Buf *b, const char *s) { while (*s) put(b, *s++); }
static void putpad(Buf *b, int d) { for (int i = 0; i < d; i++) put(b, ' '); }

static char *infer(const JVal *v, int indent) { /* returns a malloc'd string */
    Buf b = {0};
    switch (v->kind) {
    case J_NULL: puts_(&b, "z.null()"); break;
    case J_BOOL: puts_(&b, "z.boolean()"); break;
    case J_NUM:  puts_(&b, "z.number()"); break;
    case J_STR:  puts_(&b, "z.string()"); break;
    case J_ARR: {
        if (v->n == 0) { puts_(&b, "z.array(z.unknown())"); break; }
        char **types = malloc((size_t)v->n * sizeof *types);
        int same = 1;                               /* one shared element type */
        for (int i = 0; i < v->n; i++) {            /* element types, +2 indent */
            types[i] = infer(v->items + i, indent + 2);
            if (strcmp(types[i], types[0])) same = 0;
        }
        if (same) {
            puts_(&b, "z.array("); puts_(&b, types[0]); puts_(&b, ")");
        } else {                                    /* union of EVERY element  */
            puts_(&b, "z.array(z.union([\n");
            for (int i = 0; i < v->n; i++) {        /* pad EVERY line of a type */
                putpad(&b, indent + 2);
                for (const char *p = types[i]; *p; p++) {
                    put(&b, *p);
                    if (*p == '\n' && p[1]) putpad(&b, indent + 2);
                }
                puts_(&b, i + 1 < v->n ? ",\n" : "\n");
            }
            puts_(&b, "]))");   /* close unpadded: pad("", indent) is "" in TS */
        }
        for (int i = 0; i < v->n; i++) free(types[i]); free(types);
        break;
    }
    case J_OBJ: {
        if (v->n == 0) { puts_(&b, "z.object({})"); break; }
        puts_(&b, "z.object({\n");                  /* fields in textual order */
        for (int i = 0; i < v->n; i++) {
            char *inner = infer(v->items + i, indent + 2);
            putpad(&b, indent + 2); puts_(&b, v->keys[i]); puts_(&b, ": ");
            puts_(&b, inner); puts_(&b, ",\n"); free(inner);
        }
        putpad(&b, indent); puts_(&b, "})");
        break;
    }
    default: puts_(&b, "z.unknown()"); break; /* unreachable for valid JSON */
    }
    put(&b, '\0'); return b.s;
}
int main(void) {
    /* {"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}} */
    JVal roles[] = {{.kind = J_STR, .str = "admin"}, {.kind = J_NUM, .num = 1}};
    const char *metaKeys[] = {"roles"};
    JVal metaItems[] = {{.kind = J_ARR, .n = 2, .items = roles}};
    JVal meta = {.kind = J_OBJ, .n = 1, .keys = metaKeys, .items = metaItems};
    const char *keys[] = {"name", "admin", "meta"};
    JVal items[] = {{.kind = J_STR, .str = "Ada"}, {.kind = J_BOOL, .n = 1}, meta};
    JVal doc = {.kind = J_OBJ, .n = 3, .keys = keys, .items = items};
    char *schema = infer(&doc, 2); printf("const User = %s;\n", schema); free(schema);
    return 0;
}

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 →