Skip to content

JSON → TypeScript — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// json-to-typescript — infer TypeScript interfaces from any JSON value. C#
// port (canonical TS: src/lib/json-to-typescript/; Go twin: cli/json-to-typescript).
// Condensed core via System.Text.Json (JsonDocument preserves key order):
// 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 / javascript.js.
using System.Text.Json;
using System.Text.RegularExpressions;

var names = new Dictionary<string, string>();
var order = new List<Obj>();

Console.WriteLine(JsonToTs(JsonDocument.Parse(
    "{\"users\":[{\"id\":1,\"name\":\"ada\"},{\"id\":2,\"active\":true}]}").RootElement, "Root"));

string Pascal(string key) // `user_id` -> `UserId`; leading digit escapes with N
{
    var parts = Regex.Split(key, "[^A-Za-z0-9]+").Where(p => p.Length > 0).ToList();
    var head = parts.Count == 0 ? "Item" : string.Concat(parts.Select(p => char.ToUpper(p[0]) + p[1..]));
    return char.IsDigit(head[0]) ? "N" + head : head;
}
string Singular(string n) => n.Length > 1 && n.EndsWith('s') && !n.EndsWith("ss") ? n[..^1] : n + "Item";

string Sig(object t) => t switch // structural fingerprint: equal shapes share a sig
{
    string s => s,
    Obj o => o.Sig,
    Arr a => a.Of is null ? "[]" : $"[{Sig(a.Of)}]",
    Uni u => "(" + string.Join("|", u.Members.Select(Sig)) + ")",
    _ => "?"
};

object Infer(JsonElement e, string hint)
{
    switch (e.ValueKind)
    {
        case JsonValueKind.Null: return "null";
        case JsonValueKind.True:
        case JsonValueKind.False: return "boolean";
        case JsonValueKind.Number: return "number";
        case JsonValueKind.String: return "string";
        case JsonValueKind.Array when e.GetArrayLength() == 0: return new Arr();
        case JsonValueKind.Array:
            return new Arr { Of = Combine(e.EnumerateArray().Select(x => Infer(x, Singular(hint))).ToList()) };
        default: // object — key order is preserved
            var props = e.EnumerateObject()
                .Select(p => (Name: p.Name, Ty: Infer(p.Value, hint + Pascal(p.Name)), Opt: false)).ToList();
            return new Obj { Props = props, Hint = hint,
                Sig = "{" + string.Join(";", props.Select(p => $"{p.Name}:{Sig(p.Ty)}")) + "}" };
    }
}

object Combine(List<object> nodes) // condensed: objects merge; the rest dedupe to single-or-union
{
    if (nodes.Count == 0) return "?";
    if (nodes.All(n => n is Obj)) return Merge(nodes.Cast<Obj>().ToList());
    var d = nodes.DistinctBy(Sig).ToList();
    return d.Count == 1 ? d[0] : new Uni { Members = d };
}

Obj Merge(List<Obj> objs) // union of keys; a key absent from some element becomes optional
{
    var keys = objs.SelectMany(o => o.Props.Select(p => p.Name)).Distinct().ToList();
    var props = keys.Select(k =>
    {
        var types = objs.SelectMany(o => o.Props.Where(p => p.Name == k).Select(p => p.Ty)).ToList();
        return (Name: k, Ty: Combine(types), Opt: types.Count < objs.Count);
    }).ToList();
    return new Obj { Props = props, Hint = objs[0].Hint,
        Sig = "{" + string.Join(";", props.Select(p => $"{p.Name}:{Sig(p.Ty)}")) + "}" };
}

void Collect(object t, string? forced = null) // first-seen naming; collisions get 2, 3, ...
{
    switch (t)
    {
        case Obj o:
            if (!names.ContainsKey(o.Sig))
            {
                var cand = forced ?? o.Hint;
                if (names.ContainsValue(cand))
                { var i = 2; while (names.ContainsValue(cand + i)) i++; cand += i; }
                names[o.Sig] = cand; order.Add(o);
            }
            foreach (var p in o.Props) Collect(p.Ty);
            break;
        case Arr a when a.Of is not null: Collect(a.Of); break;
        case Uni u: foreach (var m in u.Members) Collect(m); break;
    }
}

string RenderKey(string k) // bareword when legal, else a quoted JSON string key
{
    string[] res = { "class", "const", "default", "function", "interface", "let", "new", "null",
                     "return", "this", "type", "var", "boolean", "number", "string", "unknown" };
    var ok = char.IsLetter(k[0]) || k[0] == '_' || k[0] == '$';
    for (var i = 1; ok && i < k.Length; i++) ok = char.IsLetterOrDigit(k[i]) || k[i] == '_' || k[i] == '$';
    return ok && !res.Contains(k) ? k : JsonSerializer.Serialize(k);
}

string RenderType(object t) => t switch
{
    string s => s == "?" ? "unknown" : s,
    Obj o => names.GetValueOrDefault(o.Sig, "unknown"),
    Uni u => string.Join(" | ", u.Members.Select(RenderType)),
    Arr a when a.Of is null => "unknown[]",
    Arr a when a.Of is Uni => $"({RenderType(a.Of)})[]", // parens around a union element
    Arr a => RenderType(a.Of) + "[]",
    _ => "unknown"
};

string JsonToTs(JsonElement e, string rootName = "Root")
{
    var root = Infer(e, Pascal(rootName));
    Collect(root, Pascal(rootName)); // only the actual root takes rootName
    var ifaces = string.Join("\n\n", order.Select(o => o.Props.Count == 0
        ? $"interface {names[o.Sig]} {{}}"
        : $"interface {names[o.Sig]} {{\n" + string.Join("\n", o.Props.Select(p =>
            $"  {RenderKey(p.Name)}{(p.Opt ? "?" : "")}: {RenderType(p.Ty)};")) + "\n}"));
    if (root is Obj) return ifaces;
    var alias = $"type {Pascal(rootName)} = {RenderType(root)};"; // non-object roots emit a type alias
    return ifaces.Length == 0 ? alias : $"{ifaces}\n\n{alias}";
}

class Obj { public List<(string Name, object Ty, bool Opt)> Props = new(); public string Hint = ""; public string Sig = ""; }
class Arr { public object? Of; }
class Uni { public List<object> Members = new(); }

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 →