Skip to main content

JSON to TypeScript

Typed models from a sample response.

Paste a representative API response and get TypeScript interfaces that describe it — or the equivalent models in Python, Java, Kotlin, Go, C#, Rust, PHP or JSDoc. The generator is deterministic: the same input always produces the same output.

Input JSON

Size: 0 BLines: 0

TypeScript models

Paste valid JSON to see the output.
Size: 0 BLines: 0

One sample in, one model out

The generator reads your document, infers a shape for every value, and merges the shapes of array items together before it emits anything. That merge is the whole trick: a 10,000-row array produces one Item interface, not ten thousand. Everything is deterministic and local — the same input always produces the same output, and nothing is sent anywhere.

Input
{
  "order_id": "a3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "total": 41.5,
  "customer": { "name": "Sarah", "email": "sarah@example.com" },
  "shipping-address": { "line1": "12 Rue Blanc", "city": "Lyon" },
  "items": [
    { "sku": "A-1", "qty": 2, "price": 9.5, "gift_note": null },
    { "sku": "B-7", "qty": 1, "price": 22.5 }
  ],
  "meta": {},
  "tags": []
}
Output
export interface Order {
  order_id: string;
  total: number;
  customer: Customer;
  "shipping-address": ShippingAddress;
  items: Item[];
  meta: Record<string, unknown>;
  tags: unknown[];
}

export interface Item {
  sku: string;
  qty: number;
  price: number;
  gift_note?: null;
}

export interface ShippingAddress {
  line1: string;
  city: string;
}

export interface Customer {
  name: string;
  email: string;
}

Nested objects become their own named interfaces rather than inline literals, and array element types are singularised — items yields Item. The root interface is emitted first so the file reads top-down.

Three judgement calls, and where they go wrong

Every generator has to answer three questions from evidence that is usually incomplete. Knowing how they are answered tells you exactly which parts of the output to check.

Is the field optional?

A field is marked optional when it appears in some objects of an array but not all. In the sample above, gift_note is in the first item and not the second, so it becomes gift_note?. A field present in every sampled item is required — which is only as true as your sample.

Can the field be null?

Nullability is separate from optionality. If a null appears anywhere for that field, the type widens: string and null across two records produce string | null. A field that is optional *and* sometimes null gets both.

What type is it?

Whole numbers become number in TypeScript but int64 in Go and int in Python, so a field that is 1 in your sample and 1.5 in production will be wrong in the typed languages. Mixed numeric evidence widens to a float; genuinely unrelated types — a field that is sometimes a string and sometimes an object — widen to unknown.

Identical shapes are emitted once

Two objects with the same field names and the same field types are the same type, whatever they are called in the document. The generator recognises this and reuses the first interface it created.

{ "billing":  { "line1": "a", "city": "b" },
  "shipping": { "line1": "c", "city": "d" } }

export interface Address {
  billing: Billing;
  shipping: Billing;
}

export interface Billing {
  line1: string;
  city: string;
}

One interface, used twice. On a real API response with a repeated address, money or pagination shape this is the difference between a readable file and a wall of near-duplicates. The name comes from whichever key was seen first, so rename it in your editor if Billing is not what you would have called it.

Keys that are not valid identifiers

JSON keys can contain hyphens, spaces and leading digits. Most target languages cannot. Each emitter handles this in the idiomatic way for that language rather than silently mangling the name, so round-tripping still works.

LanguageHow `shipping-address` is emitted
TypeScript"shipping-address": ShippingAddress; — the key is quoted.
GoShippingAddress ShippingAddress json:"shipping-address"`` — renamed, tagged.
Rust#[serde(rename = "shipping-address")] above a snake_case field.
Pythonshipping_address: ShippingAddress # JSON key: "shipping-address"
C#[JsonPropertyName("shipping-address")] above a PascalCase property.

Names that collide with a language keyword get a Model suffix, and names that start with a digit are prefixed. Repeated type names get a numeric suffix rather than overwriting each other.

The same model in nine languages

The language emitters all render one shared model, so switching language changes the syntax and the idioms but never the inferred structure.

Go
type Item struct {
	Sku      string      `json:"sku"`
	Qty      int64       `json:"qty"`
	Price    float64     `json:"price"`
	GiftNote interface{} `json:"gift_note,omitempty"`
}
Rust (serde)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
    pub name: String,
    pub email: String,
}
Python (dataclasses)
@dataclass
class Customer:
    name: str
    email: str

Also available: JavaScript JSDoc typedefs, Java, Kotlin (@Serializable data classes), C# and PHP. Optional fields become omitempty in Go, Optional[...] in Python and nullable types elsewhere.

Give it a representative sample

The quality of the output is bounded entirely by the quality of the input. A one-record sample produces types where everything is required and nothing is nullable, which is the most confident and least accurate result possible.

  1. Use a real response with several records, not a documentation example.
  2. Include records you know are incomplete — the ones missing optional fields are what makes ? appear in the right places.
  3. Include at least one record where the nullable fields are actually populated, or they will be typed as null.
  4. Check the result against the analyzer, which reports exactly which fields are missing from which records and which change type between them.

Up to 5,000 array items are sampled when merging shapes. Beyond that the model is already stable and the extra items cost time without changing the answer.

Frequently asked questions

Is this AI generated?
No. It is a deterministic inference pass over your document — shapes are merged, named and rendered by each language emitter. The same input always produces byte-identical output, and no request is made to any model or server.
How are optional fields decided?
A field is optional when it appears in some objects of an array but not in all of them. With a single-record sample nothing can be optional, which is why a multi-record sample matters.
Why is one of my fields typed as `null` or `unknown`?
null means every sampled value for that field was null, so there was no evidence of its real type. unknown means the values disagreed in a way that has no common type — a string in one record and an object in another.
Which languages are supported?
TypeScript, JavaScript (JSDoc), Python dataclasses, Java, Kotlin, Go, C#, Rust with serde, and PHP — nine in total, all from the same inferred model.
Why did two differently named objects become one interface?
Because they have identical fields and identical field types, which makes them the same type. Reusing it keeps the output readable; rename it in your editor if the inherited name does not fit.
Can I generate types from a huge array?
Yes. Up to 5,000 items are sampled to build the model, which is far more than needed for the shape to stabilise, and the output is one type per distinct shape regardless of row count.
Do whole numbers become integers?
In languages that distinguish them, yes — qty: 2 becomes int64 in Go and int in Python. If that field can hold a fraction in production, make sure your sample contains one, or the generated type will be too narrow.