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++ (C++20) port of the json-to-zod tool:
// the recursive InferZod walk from jsonToZod.ts / go.go. The stdlib has no
// JSON engine, so the demo builds the value tree from variant nodes; object
// fields are vector<pair> so textual key order survives.
#include <algorithm>
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
#include <variant>
#include <vector>

struct JVal;
using JArr = std::vector<JVal>;
using JObj = std::vector<std::pair<std::string, JVal>>; // key order preserved

struct JVal {
    std::variant<std::nullptr_t, bool, double, std::string, JArr, JObj> v;
};

static JVal jstr(const char *s) { return JVal{std::string(s)}; }
static JVal jnum(double d)      { return JVal{d}; }
static JVal jbool(bool b)       { return JVal{b}; }
static JVal jarr(JArr a)        { return JVal{std::move(a)}; }
static JVal jobj(JObj o)        { return JVal{std::move(o)}; }

static std::string pad(int n) { return std::string(static_cast<size_t>(n), ' '); }

// Infer one element schema; holds_alternative dispatch mirrors the TS switch.
static std::string infer(const JVal &val, int indent) {
    const auto &x = val.v;
    if (std::holds_alternative<std::nullptr_t>(x)) return "z.null()";
    if (std::holds_alternative<bool>(x)) return "z.boolean()";
    if (std::holds_alternative<double>(x)) return "z.number()";
    if (std::holds_alternative<std::string>(x)) return "z.string()";
    if (const auto *arr = std::get_if<JArr>(&x)) {
        if (arr->empty()) return "z.array(z.unknown())";
        std::vector<std::string> types;
        for (const JVal &e : *arr) types.push_back(infer(e, indent + 2));
        bool same = true;                     // one shared element type
        for (const auto &t : types) if (t != types[0]) same = false;
        if (same) return "z.array(" + types[0] + ")";
        std::string joined;                   // union of EVERY element
        for (size_t i = 0; i < types.size(); ++i)
            joined += types[i] + (i + 1 < types.size() ? ",\n" : "");
        std::string out = "z.array(z.union([\n";
        std::istringstream lines{joined};
        for (std::string line; std::getline(lines, line);) // pad EVERY line
            out += pad(indent + 2) + line + "\n";
        return out + "]))"; // close unpadded — pad("", indent) is "" in TS
    }
    const auto *obj = std::get_if<JObj>(&x);
    if (obj->empty()) return "z.object({})";
    std::string out = "z.object({\n";            // fields in textual order
    for (const auto &[key, fval] : *obj)
        out += pad(indent + 2) + key + ": " + infer(fval, indent + 2) + ",\n";
    return out + pad(indent) + "})";
}

int main() {
    // {"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}
    JVal doc = jobj({
        {"name", jstr("Ada")},
        {"admin", jbool(true)},
        {"meta", jobj({{"roles", jarr({jstr("admin"), jnum(1)})}})},
    });
    std::cout << "const User = " << infer(doc, 2) << ";\n";
}

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 →