JSON → TypeScript — Kotlin source
Paste any JSON and instantly get clean, typed TypeScript interfaces - primitives, nested objects, arrays and unions, all inferred. Optional keys, reserved-word quoting, and shape dedup are handled for you. Runs 100% in your browser.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// json-to-typescript — infer TypeScript interfaces from any JSON value.
// Kotlin port (canonical TS: src/lib/json-to-typescript/; Go twin:
// cli/json-to-typescript). No stdlib JSON, so a minimal order-preserving
// parser feeds the condensed core: primitives map to TS primitives, arrays
// merge element types (objects merge keys; keys absent from some element
// become optional), and shapes dedupe by structural signature into named
// interfaces. Full port with unionArrays/optionalNullable: python.py.
class JParser(val s: String) { // values need only a kind; keys keep their text
var i = 0
fun ws() { while (i < s.length && s[i].isWhitespace()) i++ }
fun str(): String { // scan a quoted string, decoding escapes
val b = StringBuilder(); i++; var esc = false
while (true) {
val c = s[i++]
when {
!esc && c == '\\' -> { esc = true; continue }
esc && c == 'u' -> { var cp = 0; repeat(4) { val h = s[i++]; cp = cp * 16 + (if (h <= '9') h - '0' else (h.code or 32) - 87) }; b.append(cp.toChar()) }
esc -> b.append(if (c == 'n') '\n' else if (c == 't') '\t' else c)
c == '"' -> return b.toString()
else -> b.append(c)
}
esc = false
}
}
fun value(): Any? { // null | Boolean | Double | String | List<Any?> | LinkedHashMap<String, Any?>
ws(); val c = s[i]
if (c == '{' || c == '[') { val obj = c == '{'; i++; ws()
val m = LinkedHashMap<String, Any?>(); val items = mutableListOf<Any?>()
if (s[i] != (if (obj) '}' else ']')) while (true) {
if (obj) { val k = str(); ws(); i++; m[k] = value() } else items.add(value())
ws(); val d = s[i++]
if (d == ',') { ws(); continue }
break
} else i++
return if (obj) m else items
}
if (c == '"') { str(); return "" } // value text unused: only the kind matters
if (s.startsWith("true", i) || s.startsWith("false", i)) { i += if (s[i] == 't') 4 else 5; return true }
if (s.startsWith("null", i)) { i += 4; return null }
val st = i
while (i < s.length && "+-.eE0123456789".indexOf(s[i]) >= 0) i++
return s.substring(st, i).toDouble()
}
}
sealed class Ty { // every node carries its structural sig — equal shapes dedupe independent of names
data class Prim(val ts: String) : Ty()
data object Unk : Ty()
data class Arr(val of: Ty?) : Ty()
data class Uni(val members: List<Ty>) : Ty()
data class Obj(val props: List<Triple<String, Ty, Boolean>>, val hint: String, val sig: String) : Ty()
}
fun pascal(key: String): String { // `user_id` -> `UserId`; a leading digit escapes with N
var out = ""; var up = true
for (c in key) if (!c.isLetterOrDigit()) up = true else { out += if (up) c.uppercaseChar() else c; up = false }
if (out.isEmpty()) out = "Item"
return if (out[0].isDigit()) "N$out" else out
}
fun singular(n: String) = if (n.length > 1 && n.endsWith('s') && !n.endsWith("ss")) n.dropLast(1) else n + "Item"
fun objSig(props: List<Triple<String, Ty, Boolean>>) =
"{" + props.joinToString(";") { "${it.first}:${sig(it.second)}" } + "}"
fun sig(t: Ty): String = when (t) {
is Ty.Prim -> t.ts
is Ty.Unk -> "?"
is Ty.Arr -> t.of?.let { "[${sig(it)}]" } ?: "[]"
is Ty.Uni -> "(" + t.members.joinToString("|") { sig(it) } + ")"
is Ty.Obj -> t.sig
}
fun infer(v: Any?, hint: String): Ty = when (v) {
null -> Ty.Prim("null")
is Boolean -> Ty.Prim("boolean")
is Double -> Ty.Prim("number")
is String -> Ty.Prim("string")
is List<*> -> Ty.Arr(if (v.isEmpty()) null else combine(v.map { infer(it, singular(hint)) }))
is Map<*, *> -> { // LinkedHashMap — insertion order kept
val props = v.entries.map { Triple(it.key as String, infer(it.value, hint + pascal(it.key as String)), false) }
Ty.Obj(props, hint, objSig(props))
}
else -> Ty.Unk // unreachable: the parser emits only the types above
}
fun combine(nodes: List<Ty>): Ty { // condensed: objects merge; the rest dedupe to single-or-union
if (nodes.isEmpty()) return Ty.Unk
val objs = nodes.filterIsInstance<Ty.Obj>()
if (objs.size == nodes.size) return mergeObjs(objs)
val d = nodes.distinctBy { sig(it) }
return if (d.size == 1) d[0] else Ty.Uni(d)
}
fun mergeObjs(objs: List<Ty.Obj>): Ty { // union of keys; a key absent from some element becomes optional
val keys = objs.flatMap { it.props.map { p -> p.first } }.distinct()
val props = keys.map { k ->
val types = objs.flatMap { o -> o.props.filter { it.first == k }.map { it.second } }
Triple(k, combine(types), types.size < objs.size)
}
return Ty.Obj(props, objs[0].hint, objSig(props))
}
val names = HashMap<String, String>(); val order = mutableListOf<Ty.Obj>()
fun collect(t: Ty, forced: String? = null) { // first-seen naming; root takes rootName; collisions get 2, 3, ...
when (t) {
is Ty.Obj -> {
if (t.sig !in names) {
var cand = forced ?: t.hint
if (cand in names.values) { var i = 2; while ("$cand$i" in names.values) i++; cand = "$cand$i" }
names[t.sig] = cand; order.add(t)
}
for (p in t.props) collect(p.second)
}
is Ty.Arr -> t.of?.let { collect(it) }
is Ty.Uni -> for (m in t.members) collect(m)
else -> {}
}
}
val RESERVED = setOf("class", "const", "default", "function", "interface", "let", "new", "null",
"return", "this", "type", "var", "boolean", "number", "string", "unknown")
fun renderKey(k: String) = // bareword when legal, else a quoted string key (no escapes needed here)
if (k.isNotEmpty() && (k[0].isLetter() || k[0] == '_' || k[0] == '$') &&
k.drop(1).all { it.isLetterOrDigit() || it == '_' || it == '$' } && k !in RESERVED) k else "\"$k\""
fun renderType(t: Ty): String = when (t) {
is Ty.Prim -> t.ts
is Ty.Unk -> "unknown"
is Ty.Obj -> names[t.sig] ?: "unknown"
is Ty.Uni -> t.members.joinToString(" | ") { renderType(it) }
is Ty.Arr -> t.of?.let { of -> if (of is Ty.Uni) "(${renderType(of)})[]" else "${renderType(of)}[]" } ?: "unknown[]"
}
fun jsonToTs(json: String, rootName: String = "Root"): String {
val root = infer(JParser(json).value(), pascal(rootName))
collect(root, pascal(rootName)) // only the actual root takes rootName
val ifaces = order.joinToString("\n\n") { o ->
if (o.props.isEmpty()) "interface ${names[o.sig]} {}"
else "interface ${names[o.sig]} {\n" + o.props.joinToString("\n") { p ->
" ${renderKey(p.first)}${if (p.third) "?" else ""}: ${renderType(p.second)};"
} + "\n}"
}
if (root is Ty.Obj) return ifaces
val alias = "type ${pascal(rootName)} = ${renderType(root)};" // non-object roots emit a type alias
return if (ifaces.isEmpty()) alias else "$ifaces\n\n$alias"
}
// shapes dedupe by signature; array objects merge keys (name?/active? optional)
fun main() = println(jsonToTs("""{"users":[{"id":1,"name":"ada"},{"id":2,"active":true}]}"""))
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 →