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# (.NET 8, top-level statements) port of
// the json-to-zod tool: parse with System.Text.Json (JsonDocument keeps
// property order) and run the recursive InferZod walk from jsonToZod.ts /
// go.go. Never throws — parse failures come back as (ok, code, error).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;

string Infer(JsonElement e, int indent = 2)
{
    switch (e.ValueKind)
    {
        case JsonValueKind.Null: return "z.null()";
        case JsonValueKind.True:
        case JsonValueKind.False: return "z.boolean()";
        case JsonValueKind.Number: return "z.number()";
        case JsonValueKind.String: return "z.string()";
        case JsonValueKind.Array:
            var items = e.EnumerateArray().ToList();
            if (items.Count == 0) return "z.array(z.unknown())";
            var types = items.Select(i => Infer(i, indent + 2)).ToList();
            var distinct = types.Distinct().ToList();
            string inner = distinct.Count == 1 ? distinct[0]
                : "z.union([\n" + string.Join("\n",           // union of EVERY element
                      string.Join(",\n", types).Split('\n')
                          .Select(l => l.Length == 0 ? l : new string(' ', indent + 2) + l))
                    + "\n])"; // close unpadded — pad("", indent) is "" in TS
            return $"z.array({inner})";
        case JsonValueKind.Object:
            var fields = e.EnumerateObject().ToList(); // order preserved
            if (fields.Count == 0) return "z.object({})";
            var body = string.Join("\n", fields.Select(f =>
                $"{new string(' ', indent + 2)}{f.Name}: {Infer(f.Value, indent + 2)},"));
            return $"z.object({{\n{body}\n{new string(' ', indent)}}})";
        default: return "z.unknown()"; // unreachable for valid JSON
    }
}

// Reduce an arbitrary string to a usable JS identifier ("9lives" -> "_lives").
string SanitizeVarName(string name)
{
    string cleaned = new string(name
        .Where(c => char.IsAsciiLetterOrDigit(c) || c == '_' || c == '$').ToArray());
    cleaned = Regex.Replace(cleaned, "^[0-9]+", m => new string('_', m.Length));
    return cleaned.Length == 0 ? "schema" : cleaned;
}

(bool Ok, string Code, string? Error) JsonToZod(string json, string rootName = "Root")
{
    try
    {
        using var doc = JsonDocument.Parse(json);
        return (true, $"const {SanitizeVarName(rootName)} = {Infer(doc.RootElement)};", null);
    }
    catch (JsonException ex) { return (false, "", ex.Message); }
}

// {"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}
Console.WriteLine(JsonToZod(
    """{"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}""", "User").Code);

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 →