Skip to content

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

// JSON -> Zod schema inference — Swift port of the json-to-zod tool: the
// recursive InferZod walk from jsonToZod.ts / go.go. Dictionary does not
// guarantee key order, so object nodes carry labeled tuples in textual order
// (the Go twin's order-preserving decoder fills the same shape).
import Foundation

indirect enum JVal {
    case null
    case bool(Bool)
    case number(Double)
    case string(String)
    case array([JVal])
    case object([(key: String, value: JVal)])
}

func inferZod(_ value: JVal, indent: Int = 2) -> String {
    switch value {
    case .null: return "z.null()"
    case .bool: return "z.boolean()"
    case .number: return "z.number()"
    case .string: return "z.string()"
    case .array(let items):
        guard !items.isEmpty else { return "z.array(z.unknown())" }
        let types = items.map { inferZod($0, indent: indent + 2) }
        let inner: String
        if Set(types).count == 1 {                 // one shared element type
            inner = types[0]
        } else {                                   // union of EVERY element
            let padded = types.joined(separator: ",\n")
                .split(separator: "\n", omittingEmptySubsequences: false)
                .map { line -> String in           // pad EVERY line
                    line.isEmpty ? "" : String(repeating: " ", count: indent + 2) + String(line)
                }
                .joined(separator: "\n")
            inner = "z.union([\n" + padded + "\n])" // close unpadded: pad("", ind) is ""
        }
        return "z.array(\(inner))"
    case .object(let fields):                      // fields in textual order
        guard !fields.isEmpty else { return "z.object({})" }
        let lines = fields.map { key, v in
            String(repeating: " ", count: indent + 2)
                + "\(key): \(inferZod(v, indent: indent + 2)),"
        }
        return "z.object({\n" + lines.joined(separator: "\n") + "\n"
            + String(repeating: " ", count: indent) + "})"
    }
}

// Reduce an arbitrary string to a usable JS identifier ("9lives" -> "_lives").
func sanitizeVarName(_ name: String) -> String {
    var cleaned = String(name.compactMap {
        ($0.isASCII && $0.isLetter) || ($0.isASCII && $0.isNumber)
            || $0 == "_" || $0 == "$" ? $0 : nil
    })
    while let first = cleaned.first, first.isNumber {
        cleaned = "_" + cleaned.dropFirst()        // each leading digit -> "_"
    }
    return cleaned.isEmpty ? "schema" : cleaned
}

// {"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}
let doc: JVal = .object([
    ("name", .string("Ada")),
    ("admin", .bool(true)),
    ("meta", .object([("roles", .array([.string("admin"), .number(1)]))])),
])
print("const User = \(inferZod(doc));")

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 →