DevToys Web Pro iconDevToys Web ProBlog
Traducido con LocalePack logoLocalePack
Valóranos:
Prueba la extensión del navegador:
← Back to Blog

JSON to TypeScript Types: Generate Interfaces Automatically

10 min read

Every backend integration starts the same way: you open the API docs, look at a 50-field JSON response, and start typing interface User {. You copy field names, guess whether createdAt is a string or a number, wonder if address can be null, and ship an interface that silently drifts from reality the moment the backend adds a field. Type generators eliminate this manual step. Paste in a JSON sample — or feed it a URL — and get a complete, accurate TypeScript interface in under a second. Use the JSON to Code converter to follow along with every example in this article.

How Type Generators Work

Type generators perform structural inference: they walk the JSON object tree and map each value to a TypeScript type based on what they observe.

{
  "id": 42,
  "name": "Alice",
  "verified": true,
  "score": 9.8,
  "tags": ["admin", "beta"],
  "address": null
}

That sample produces:

export interface User {
  id:       number;
  name:     string;
  verified: boolean;
  score:    number;
  tags:     string[];
  address:  null;
}

The inference rules are straightforward: JSON numbers become number, strings become string, booleans become boolean, arrays become typed arrays, and nested objects become nested interfaces. The tricky part is what happens when the generator sees multiple samples of the same type — it merges them with union types.

Feed two samples where score is sometimes a number and sometimes a string:

// Sample 1
{ "score": 9.8 }

// Sample 2
{ "score": "N/A" }
export interface User {
  score: number | string;
}

This union behavior is why feeding multiple real API responses to the generator produces more accurate types than relying on a single example. A single sample can make optional fields look required, or nullable fields look non-null.

The Nullable vs Optional Pitfall

This is the most common source of inaccurate generated types. TypeScript draws a sharp distinction between three different states:

JSONTypeScript typeMeaning
"field": "value"field: stringAlways present, never null
"field": nullfield: string | nullPresent in the object, but value is null
key absent from objectfield?: stringKey may not exist at all
key absent or nullfield?: string | nullKey may be absent or present with null value

A generator looking at one sample where address is null will emit address: null — which is technically correct for that sample but useless in practice. The real type should be address: Address | null.

Conversely, a field that is simply absent in one sample but present in another will be marked optional (field?: string) when fed both samples. This is the correct behavior — but it only works if you give the generator enough samples to observe both states.

Rule: Always provide at least two samples: one where every optional field is present, and one where optional fields are absent or null. The generator merges the observations into the most accurate possible type.

Array Handling

Arrays introduce their own inference challenges:

{
  "items": [],
  "tags": ["alpha", "beta"],
  "mixed": [1, "two", true]
}
export interface Response {
  items: never[];           // empty array — no element type observed
  tags:  string[];          // homogeneous string array
  mixed: Array<number | string | boolean>; // union from mixed types
}

The never[] type for an empty array is technically correct — there are no elements to infer from — but it is almost always wrong in practice. When you see never[], replace it manually with the expected element type. Some generators (like quicktype) emit any[] instead, which is more permissive but equally imprecise.

For homogeneous arrays of objects, the generator creates a named interface for the element type and references it:

{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}
export interface Root {
  users: User[];
}

export interface User {
  id:   number;
  name: string;
}

Recursive Types

Tree structures and nested comment threads produce recursive type definitions — interfaces that reference themselves. Most generators handle shallow recursion correctly:

{
  "id": 1,
  "text": "Parent comment",
  "replies": [
    {
      "id": 2,
      "text": "Child comment",
      "replies": []
    }
  ]
}
export interface Comment {
  id:      number;
  text:    string;
  replies: Comment[];
}

TypeScript handles self-referential interfaces natively, so this output is immediately usable. Problems arise with deep mutual recursion (type A references type B which references type A) and with generators that detect the recursion but give up, emitting any instead. If you see any in a generated type, check whether it is covering a recursive reference and supply the correct type manually.

Quicktype Walkthrough

Quicktype is the most capable open-source type generator available. Install it globally:

npm install -g quicktype

Generate TypeScript interfaces from a local JSON file:

quicktype user.json -o User.ts --lang typescript

Generate directly from a URL (quicktype fetches the response and infers types from it):

quicktype https://api.example.com/users/1 -o User.ts --lang typescript

Feed multiple samples for more accurate union types. Pass them as separate files or via stdin:

# Multiple files — quicktype merges all samples
quicktype sample1.json sample2.json sample3.json -o User.ts --lang typescript

# From a JSON Lines file (one JSON object per line)
quicktype --src-lang json-lines responses.jsonl -o User.ts --lang typescript

Quicktype also generates a Convert module with parse and stringify helpers by default. To get bare interfaces only:

quicktype user.json -o User.ts --lang typescript --just-types

The --nice-property-names flag converts snake_case JSON keys to camelCase TypeScript properties and adds a /* original: snake_case */ comment for traceability.

Alternative Approaches

Quicktype is not the only option. Each tool has a different tradeoff between automation and control:

  • json-to-ts (npm) — lightweight Node.js library, easy to embed in scripts or build tools. Less accurate than quicktype for edge cases but zero config.
  • ts-morph — TypeScript AST manipulation library. Lets you generate or transform types programmatically with full control over output formatting. Best when you need to post-process generated types (add decorators, rename fields, inject JSDoc).
  • Zod schema inference — instead of generating TypeScript interfaces directly, generate a Zod schema. You get both the type and runtime validation in one step. See the JSON Formatter Guide for JSON validation patterns that pair well with Zod.
  • OpenAPI / JSON Schema generators — if your API has an OpenAPI spec, tools like openapi-typescript generate types directly from the spec rather than from runtime samples. More reliable for large APIs.

Target Languages

LanguageOutput typeWhat quicktype handles wellWatch out for
TypeScriptinterfaces + type aliasesUnion types, optional fields, genericsEmpty arrays need manual annotation
Gostructs with json tagsjson:"field,omitempty", pointer types for nullableNo union types — uses interface{}
Pythondataclasses or TypedDictOptional[str], nested dataclassesRuntime validation still manual
Ruststructs with serde derivesOption<T>, #[serde(rename)]Mixed-type arrays require enum variants
Javaclasses with Jackson annotations@JsonProperty, nullable via @NullableVerbose; consider Kotlin data classes instead
C#classes with Newtonsoft / System.Text.JsonJsonPropertyName, nullable reference typesEnable #nullable enable for best results

Try all these targets in the browser with the JSON to Code converter — select the output language from the dropdown and the types update instantly.

Runtime Validation

TypeScript types are erased at compile time. An interface says nothing about what arrives over the wire at runtime. If the API changes a field type, your code will silently receive the wrong shape — no TypeScript error, no runtime exception, just wrong data flowing through your application.

The solution is to add a validation layer at the API boundary. The most popular options:

// Zod — define schema once, derive type from it
import { z } from "zod";

const UserSchema = z.object({
  id:       z.number(),
  name:     z.string(),
  verified: z.boolean(),
  score:    z.number(),
  tags:     z.array(z.string()),
  address:  z.string().nullable(),
});

type User = z.infer<typeof UserSchema>;

// Parse and validate at the API boundary
const user = UserSchema.parse(await response.json());
// Valibot — similar API, smaller bundle
import { object, number, string, boolean, array, nullable, parse } from "valibot";

const UserSchema = object({
  id:       number(),
  name:     string(),
  verified: boolean(),
  score:    number(),
  tags:     array(string()),
  address:  nullable(string()),
});

type User = InferOutput<typeof UserSchema>;

Zod can also be generated from a JSON sample using the json-to-zod CLI:

npx json-to-zod -s user.json -o UserSchema.ts

For validation-heavy pipelines, consider AJV (JSON Schema validator) with generated JSON Schema definitions — AJV compiles schemas to fast validation functions, making it suitable for high-throughput API servers.

See the API debug workflow guide for patterns around catching these mismatches early during development.

Recommended Workflow

A repeatable workflow for integrating a new API endpoint into a TypeScript project:

  1. Collect samples. Fetch at least two real responses — one "happy path" (all fields populated) and one edge case (optional fields absent, arrays empty, nullable fields null).
  2. Generate types. Run quicktype (or use the JSON to Code converter in the browser) against both samples simultaneously.
  3. Audit the output. Look for never[], any, and bare null types. Each signals a gap that needs a real-world sample or manual annotation.
  4. Add runtime validation. Wrap the generated interface in a Zod schema (or equivalent) at the API fetch boundary.
  5. Check types into the repo. Treat generated types as source code — commit them alongside your other TypeScript files. Do not generate at build time unless the API has a machine-readable spec (OpenAPI, GraphQL schema) to generate from deterministically.
  6. Regenerate when the API changes. Keep the sample JSON files in the repo alongside the generated types so teammates can regenerate with a single command.
# One-liner to refresh types from saved samples
quicktype samples/user-full.json samples/user-partial.json \
  -o src/types/User.ts --lang typescript --just-types

Skip the manual typing. Paste your JSON into the JSON to Code converter and get TypeScript interfaces, Go structs, Python dataclasses, Rust serde types, and more — all in one click, all processed locally in your browser.