JSON → TypeScript — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// json-to-typescript — infer TypeScript interfaces from any JSON value.
// Swift port (canonical TS: src/lib/json-to-typescript/; Go twin:
// cli/json-to-typescript). JSONSerialization loses key order, so — like the
// json-minify port — this is a hand-rolled scanner feeding 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: python.py.
enum Jv { case nul, bool, num, str, arr([Jv]), obj([(String, Jv)]) } // values need only a kind
struct Scanner {
let s: [UInt8]; var i = 0
mutating func ws() { while i < s.count && (s[i] == 32 || s[i] == 9 || s[i] == 10 || s[i] == 13) { i += 1 } }
mutating func str() -> String { // scan a quoted string, decoding escapes
var out = [UInt8](); i += 1; var esc = false
while true {
let c = s[i]; i += 1
if !esc && c == 0x5C { esc = true; continue }
if esc && c == 0x75 { // 'u' — 4 hex digits
var cp = 0
for _ in 0..<4 { let h = s[i]; i += 1; cp = cp * 16 + (h <= 0x39 ? Int(h - 0x30) : Int((h | 32) - 87)) }
out.append(contentsOf: Array(String(UnicodeScalar(cp)!).utf8))
} else if esc { out.append(c == 0x6E ? 10 : c == 0x74 ? 9 : c) }
else if c == 0x22 { return String(decoding: out, as: UTF8.self) }
else { out.append(c) }
esc = false
}
}
mutating func val() -> Jv {
ws(); let c = s[i]
if c == 0x7B || c == 0x5B { // '{' / '[' — objects keep key order
let obj = c == 0x7B; i += 1; ws()
var pairs: [(String, Jv)] = []; var items: [Jv] = []
if s[i] != (obj ? 0x7D : 0x5D) {
while true {
if obj { let k = str(); ws(); i += 1; pairs.append((k, val())) } else { items.append(val()) }
ws(); let d = s[i]; i += 1
if d == 0x2C { ws(); continue } // ','
break // '}' / ']'
}
} else { i += 1 }
return obj ? .obj(pairs) : .arr(items)
}
if c == 0x22 { _ = str(); return .str } // value text unused: only the kind matters
if c == 0x74 || c == 0x66 { i += (c == 0x74 ? 4 : 5); return .bool } // true / false
if c == 0x6E { i += 4; return .nul } // null
while i < s.count && (s[i] == 0x2D || s[i] == 0x2B || s[i] == 0x2E || s[i] == 0x65 || s[i] == 0x45 || (s[i] >= 0x30 && s[i] <= 0x39)) { i += 1 }
return .num
}
}
struct ObjT { var props: [(String, Ty, Bool)]; var hint: String; var sig: String }
indirect enum Ty { case prim(String), unk, arr(Ty?), obj(ObjT), uni([Ty]) }
func sig(_ t: Ty) -> String { // structural fingerprint: equal shapes share a sig
switch t {
case .prim(let s): return s
case .unk: return "?"
case .arr(let of): return of.map { "[\(sig($0))]" } ?? "[]"
case .uni(let ms): return "(" + ms.map(sig).joined(separator: "|") + ")"
case .obj(let o): return o.sig
}
}
func pascal(_ key: String) -> String { // `user_id` -> `UserId`; a leading digit escapes with N
var out = ""; var up = true
for c in key {
if c.isLetter || c.isNumber { out += up ? String(c).uppercased() : String(c); up = false } else { up = true }
}
if out.isEmpty { out = "Item" }
if let f = out.first, f.isNumber { out = "N" + out }
return out
}
func singular(_ n: String) -> String { n.count > 1 && n.hasSuffix("s") && !n.hasSuffix("ss") ? String(n.dropLast()) : n + "Item" }
func objSig(_ props: [(String, Ty, Bool)]) -> String { "{" + props.map { "\($0.0):\(sig($0.1))" }.joined(separator: ";") + "}" }
func infer(_ v: Jv, _ hint: String) -> Ty {
switch v {
case .nul: return .prim("null")
case .bool: return .prim("boolean")
case .num: return .prim("number")
case .str: return .prim("string")
case .arr(let items): return .arr(items.isEmpty ? nil : combine(items.map { infer($0, singular(hint)) }))
case .obj(let pairs):
let props = pairs.map { ($0.0, infer($0.1, hint + pascal($0.0)), false) }
return .obj(ObjT(props: props, hint: hint, sig: objSig(props)))
}
}
func combine(_ nodes: [Ty]) -> Ty { // condensed: objects merge; the rest dedupe to single-or-union
if nodes.isEmpty { return .unk }
var objs: [ObjT] = []
for case .obj(let o) in nodes { objs.append(o) }
if objs.count == nodes.count { return mergeObjs(objs) }
var d: [Ty] = []
for n in nodes where !d.contains(where: { sig($0) == sig(n) }) { d.append(n) }
return d.count == 1 ? d[0] : .uni(d)
}
func mergeObjs(_ objs: [ObjT]) -> Ty { // union of keys; a key absent from some element becomes optional
var keys: [String] = []
for o in objs { for p in o.props where !keys.contains(p.0) { keys.append(p.0) } }
let props = keys.map { k -> (String, Ty, Bool) in
let types = objs.flatMap { o in o.props.filter { $0.0 == k }.map { $0.1 } }
return (k, combine(types), types.count < objs.count)
}
return .obj(ObjT(props: props, hint: objs[0].hint, sig: objSig(props)))
}
var names: [String: String] = [:]; var order: [ObjT] = []
func collect(_ t: Ty, _ forced: String?) { // first-seen naming; root takes rootName; collisions get 2, 3, ...
switch t {
case .obj(let o):
if names[o.sig] == nil {
var cand = forced ?? o.hint
if names.values.contains(cand) { var i = 2; while names.values.contains("\(cand)\(i)") { i += 1 }; cand = "\(cand)\(i)" }
names[o.sig] = cand; order.append(o)
}
for p in o.props { collect(p.1, nil) }
case .arr(let of): if let of { collect(of, nil) }
case .uni(let ms): for m in ms { collect(m, nil) }
default: break
}
}
let RESERVED: Set<String> = ["class", "const", "default", "function", "interface", "let", "new", "null",
"return", "this", "type", "var", "boolean", "number", "string", "unknown"]
func renderKey(_ k: String) -> String { // bareword when legal, else a quoted string key (no escapes needed here)
let ok = !k.isEmpty && (k.first!.isLetter || k.first! == "_" || k.first! == "$")
&& !k.dropFirst().contains { !($0.isLetter || $0.isNumber || $0 == "_" || $0 == "$") } && !RESERVED.contains(k)
return ok ? k : "\"\(k)\""
}
func renderType(_ t: Ty) -> String {
switch t {
case .prim(let s): return s
case .unk: return "unknown"
case .obj(let o): return names[o.sig] ?? "unknown"
case .uni(let ms): return ms.map(renderType).joined(separator: " | ")
case .arr(let of):
guard let of else { return "unknown[]" }
if case .uni = of { return "(\(renderType(of)))[]" } // parens around a union element
return renderType(of) + "[]"
}
}
func jsonToTs(_ json: String, _ rootName: String = "Root") -> String {
var sc = Scanner(s: Array(json.utf8))
let root = infer(sc.val(), pascal(rootName))
collect(root, pascal(rootName)) // only the actual root takes rootName
var out: [String] = []
for o in order {
if o.props.isEmpty { out.append("interface \(names[o.sig]!) {}"); continue }
let lines = o.props.map { " \(renderKey($0.0))\($0.2 ? "?" : ""): \(renderType($0.1));" }.joined(separator: "\n")
out.append("interface \(names[o.sig]!) {\n\(lines)\n}")
}
if case .obj = root { return out.joined(separator: "\n\n") }
let alias = "type \(pascal(rootName)) = \(renderType(root));" // non-object roots emit a type alias
return out.isEmpty ? alias : out.joined(separator: "\n\n") + "\n\n" + alias
}
// shapes dedupe by signature; array objects merge keys (name?/active? optional)
print(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 →