Skip to content

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

// JSON -> Zod schema inference — Kotlin port of the json-to-zod tool: the
// recursive InferZod walk from jsonToZod.ts / go.go. Objects are JObj field
// lists so textual key order survives (the tool's parity requirement).
class JObj(vararg val fields: Pair<String, Any?>)

fun inferZod(value: Any?, indent: Int = 2): String = when (value) {
    null -> "z.null()"
    is Boolean -> "z.boolean()"
    is Number -> "z.number()"
    is String -> "z.string()"
    is List<*> -> {
        if (value.isEmpty()) "z.array(z.unknown())"
        else {
            val types = value.map { inferZod(it, indent + 2) }
            val inner = if (types.distinct().size == 1) types.first()
            else {
                val padded = types.joinToString(",\n")   // union of EVERY element
                    .split("\n").joinToString("\n") {    // pad EVERY line
                        if (it.isEmpty()) it else " ".repeat(indent + 2) + it
                    }
                "z.union([\n$padded\n])"   // close unpadded: pad("", indent) is ""
            }
            "z.array($inner)"
        }
    }
    is JObj -> {
        if (value.fields.isEmpty()) "z.object({})"
        else buildString {                        // fields in textual order
            append("z.object({\n")
            for ((key, v) in value.fields)
                append(" ".repeat(indent + 2)).append(key).append(": ")
                    .append(inferZod(v, indent + 2)).append(",\n")
            append(" ".repeat(indent)).append("})")
        }
    }
    else -> "z.unknown()"                         // unreachable for valid JSON
}

// Reduce an arbitrary string to a usable JS identifier ("9lives" -> "_lives").
fun sanitizeVarName(name: String): String {
    val cleaned = name.filter { it.isLetterOrDigit() || it == '_' || it == '$' }
        .replace(Regex("^[0-9]+")) { m -> "_".repeat(m.value.length) }
    return cleaned.ifEmpty { "schema" }
}

fun jsonToZod(tree: Any?, rootName: String = "Root"): Result<String> =
    runCatching { "const ${sanitizeVarName(rootName)} = ${inferZod(tree)};" }

fun main() {
    // {"name": "Ada", "admin": true, "meta": {"roles": ["admin", 1]}}
    val doc = JObj(
        "name" to "Ada",
        "admin" to true,
        "meta" to JObj("roles" to listOf("admin", 1))
    )
    println(jsonToZod(doc, "User").getOrThrow())
}

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 →