JSON to Types, Explained

After reading this you will understand how a type generator reads one JSON sample and turns it into TypeScript interfaces, Python dataclasses or Go structs, and where the guesswork can quietly bite you.

What it does and one quick example

The tool reads a JSON value and produces static type definitions in the language you pick. It does this by walking the JSON tree, recording the type of every value it meets, and naming every nested object so it becomes its own type.

Consider this input:

{"id": 42, "name": "Ada", "active": true}

In TypeScript that becomes:

interface Root { id: number; name: string; active: boolean; }

Three fields, three inferred types. JSON has only six value kinds (object, array, string, number, boolean, null), so the mapping to a target language is short and mechanical. The interesting work happens with nesting, arrays, and the values that are missing or null.

When to use it, and when not

Reach for a type generator when you have a concrete JSON payload and want a typed skeleton fast. A response from an API you are integrating, a config file, a fixture in a test suite: paste it, get types, adjust by hand. It saves the tedious transcription and it rarely makes a typo.

Do not treat the output as a contract. The generator sees one sample. If the real API sometimes returns null for a field that was a string in your sample, or omits a field entirely, the generated type will be wrong for those cases. When a formal schema already exists (OpenAPI, JSON Schema, a .proto file), generate from that instead. A schema states what can happen; a sample only shows what did happen once.

A field that is null or absent in your sample gives the generator no type information. It will guess (often any, None, or a nullable), and that guess is the first thing to widen by hand.

The inference rules, stated plainly

There is no probability here and no threshold to tune. The generator applies a fixed set of rules. Understanding them is understanding the whole tool.

Scalars
A JSON string becomes string / str / string. A boolean becomes boolean / bool / bool. A number becomes number in TypeScript, and splits by whether it has a fractional part in Python and Go: 1 reads as int, 1.5 as float.
Objects
Each object becomes a named type. Nested objects are lifted out into their own named types rather than being inlined, so a three-level document produces three types.
Arrays
The element type is inferred from all elements. If every element agrees, that shared type wins. If they disagree, the result is a union (TypeScript), or a fallback to any / Any / interface{}.
null
A lone null carries no type. Combined with a real value elsewhere in an array, it usually becomes a nullable or optional version of that value's type.

The array rule is worth a formula. For an array with elements of observed types t_1, t_2, \ldots, t_n, the inferred element type is the union of the distinct types:

T = \bigcup_{i=1}^{n} \text{type}(x_i)

Here x_i is the i-th element, \text{type}(x_i) is its inferred type, and T is the element type of the whole array. If the union collapses to one type, you get a clean string[]. If it has two members, TypeScript gives (string | number)[], while Go and Python fall back to a single loose type because neither expresses anonymous unions well.

A worked example with the demo data

Load the demo sample. It is a small user object with a nested address and a list of roles:

{"id": 42, "name": "Ada Lovelace", "email": "[email protected]", "active": true, "roles": ["admin", "editor"], "address": {"city": "London", "zip": "WC1"}, "score": 9.5}

Walking the tree by hand

  1. id is 42, an integer number. TypeScript: number. Python: int. Go: int.
  2. name and email are strings. Straightforward.
  3. active is true: a boolean.
  4. roles is an array of two strings. Both elements agree, so the union collapses: element type is string. TypeScript: string[]. Go: []string. Python: list[str].
  5. address is an object, so it is lifted into its own type named after the field: an Address interface with city: string and zip: string.
  6. score is 9.5, a number with a fractional part. TypeScript: number. Python: float. Go: float64.

The TypeScript output is two interfaces:

interface Address { city: string; zip: string; }

interface Root { id: number; name: string; email: string; active: boolean; roles: string[]; address: Address; score: number; }

Notice that id and score both map to number in TypeScript, because JSON and JavaScript have a single number type. The int/float split only appears in Python and Go. That single fact explains most of the surprise when the same input produces different-looking output across the three languages.

How the same sample differs across languages

The three targets disagree on exactly two things: how they split numbers, and how they express "this could also be null or missing". The table below shows the mapping for the demo fields.

Inferred type per field, by target language
FieldJSON valueTypeScriptPythonGo
id42numberintint
score9.5numberfloatfloat64
activetruebooleanboolbool
roles["admin","editor"]string[]list[str][]string
address{...}AddressAddressAddress

Go adds one detail the others skip: struct tags. A field named zip in JSON becomes an exported Go field Zip with a tag `json:"zip"`, because Go capitalises exported names but the wire format keeps the lowercase key. Watch for that tag; without it, decoding silently drops the field.

If your JSON keys use snake_case (created_at) and you want idiomatic Python or Go, rename in the generated types and rely on the JSON tag or a field alias to keep the mapping. Do not rename the JSON key itself unless you control the producer.

Interpreting arrays and unions

Arrays are where a single sample lies to you most often. Suppose roles had been ["admin", 7] by accident. The union rule then gives T = \text{string} \cup \text{number}, and you would see (string | number)[] in TypeScript. That is the generator being honest about a messy sample, not a bug. Fix the data, regenerate, and the type tightens back to string[].

The reverse problem is more dangerous. An empty array [] gives no elements, so the union is empty. There is nothing to infer. TypeScript falls back to any[] or never[], Python to list, Go to []interface{}. All three are placeholders you must replace with the intended element type by hand.

The widget below lets you feel the union rule directly.

With a homogeneous array like ["a","b","c"] the inferred element type is string, shown as string[]. Add a number to make ["a","b",3] and the type widens to (string | number)[]. Add null to get (string | number | null)[]. An empty array collapses to any[] because there is nothing to observe.

Common mistakes

Four errors account for most of the trouble people hit.

  • Treating optional as required. If your sample happens to include every field, the generator marks all of them required. Real payloads often omit fields. Add ? in TypeScript, Optional[...] or a default in Python, or a pointer type in Go where a field may be absent.
  • Assuming int where float is possible. A sample value of 0 reads as int, but if the field can hold 0.5 you need float. JSON does not distinguish; only the observed digits do.
  • Ignoring empty containers. An empty array or empty object gives the generator nothing to work with. Feed a sample that actually contains data.
  • Copying the type name. Nested objects get named from their field, so two different objects both called data can collide. Rename generated types to something specific.

All of this runs in your browser. The JSON you paste is never sent anywhere, which matters when the sample is a real production payload with real user data in it.

Related tools

Once you have types, you often need data to fill them. The Test Data Generator produces fake names, emails and addresses as JSON you can shape to match. To inspect structured formats that are not JSON, the XML Formatter & Validator pretty-prints and validates XML. For contact and calendar records specifically, the vCard Generator & Parser and the iCalendar Generator & Parser read and write those formats. If you want to see numeric fields at a glance, paste them into the Chart Maker.

Frequently asked questions

Why does the same number become int in Python but number in TypeScript?

JSON and JavaScript have one numeric type. TypeScript inherits that, so every number is number. Python and Go have separate integer and floating types, so the generator inspects each value: 42 reads as int, 9.5 reads as float. If a field can hold both, widen it to float by hand.

What happens with a field that is null in my sample?

A lone null carries no type, so the generator emits a placeholder: any or a nullable in TypeScript, Optional[Any] in Python, interface{} or a pointer in Go. Replace it with the type the field really holds when it is not null.

Can I generate types from several samples at once?

Put them in one array and paste that. The array rule then unions the element types across all your samples, which is exactly how you catch a field that is a string in one record and null in another.

Does the tool upload my JSON anywhere?

No. Everything runs locally in your browser. The sample never leaves your device, so pasting real payloads is safe.

Why did my nested object become its own type instead of being inlined?

By design. Breaking nested objects into named types keeps each type short and lets you reuse and reference them. A three-level document produces three named types rather than one deeply nested one.