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 →