Skip to content

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 →