JSON to Types

JSON to TypeScript types and Zod schemas

Paste a JSON sample. Every array element is merged into one type, optional and nullable are kept distinct, and anything the sample cannot establish is flagged rather than guessed.

An array of one object shape.

1 interface · 4 fields · 3 levels deep

Where this sample is ambiguous

  • Check this

    Optionality inferred from one sample

    `[].tags` and `[].manager` were missing from at least one object in the sample and are therefore marked optional. A single sample cannot distinguish "sometimes omitted" from "omitted in this particular payload".

    Fix: Check these against the real schema. Absent means optional; a null value would instead mean present-and-nullable, which is a different type.

  • Note

    Whole numbers are still `number`

    Every numeric value in the sample was a whole number, but JSON has a single number type and cannot express the difference. The output uses `number`.

    Fix: If a field is genuinely an integer, tighten it by hand: `z.number().int()`.

TypeScript

export interface RootItem {
  id: number;
  name: string;
  tags?: string[];
  manager?: null;
}

export type Root = RootItem[];

Zod

import { z } from 'zod';

export const RootItemSchema = z.object({
  id: z.number(),
  name: z.string(),
  tags: z.array(z.string()).optional(),
  manager: z.null().optional(),
});

export const RootSchema = z.array(RootItemSchema);

Worked examples

Built for agents too

The same engine is available as a JSON API and an MCP server, so a coding agent can call it instead of writing types by hand — cheaper in tokens, and it does not make the array mistake.

curl -X POST 'https://json-to-types.gumballtools.com/api/v1/convert' \
  -H 'Content-Type: application/json' \
  -d '{"json":"{\"id\":1,\"tags\":[]}"}'

API and MCP setup · llms.txt

Questions people actually ask

Why does typing from the first array element go wrong?

Because JSON arrays are not required to be homogeneous. If the first user object has no "admin" key and the second does, a type derived from element zero will reject element one — and TypeScript will happily compile code that then fails at runtime against real data. This tool merges every element in the array into a single type, so a key present in any element appears in the output, marked optional if it was missing from at least one.

What is the difference between an optional and a nullable field?

An optional field may be absent from the object entirely: `{ "a"?: string }`. A nullable field is always present but may hold null: `{ "a": string | null }`. They are different types and require different runtime checks — `"a" in obj` versus `obj.a !== null`. This tool distinguishes them: a key missing from some objects in a sample becomes optional, and a null value becomes a nullable union.

What happens with an empty array or an empty object?

Nothing can be inferred from them, so the output uses `unknown[]` for an empty array and `Record<string, unknown>` for an empty object, and emits a warning naming the path. Guessing a shape here would be worse than admitting ignorance: you would get a confident-looking type with no evidence behind it. Supply a sample where those containers are populated and the types will sharpen.

Can it tell whether a number is an integer?

No, and neither can anything else working from a JSON sample. JSON has exactly one number type; 10 and 10.0 are indistinguishable once parsed. If every value observed for a field was a whole number the tool notes it as an informational warning, but it still emits `number`, because a sample of whole numbers is not evidence that a float can never appear. Tighten it by hand with `z.number().int()` if you know the real constraint.

How are nested interfaces named?

From the key path. A `user` key containing an object produces a `User` interface, and an array under `categories` produces `Category` for its element type. Structurally identical shapes are deduplicated, so a billing address and a shipping address with the same fields share one interface rather than generating two identical declarations. Names that would collide get a numeric suffix.

What about keys that are not valid identifiers?

Keys like `content-type`, `x-request-id`, or reserved words are quoted in both the TypeScript and the Zod output, which is valid in both, and flagged with a warning. You will need bracket access — `payload["content-type"]` — rather than dot notation to read them.

Does it accept a JSON Schema or an OpenAPI document?

No. This works from concrete sample data — an actual API response or payload. If you already have a JSON Schema or an OpenAPI document you have a formal declaration of the types, including constraints a sample cannot express, and you should generate from that instead. This tool exists for the common case where all you have is a response you captured.

How big a sample can I paste?

Up to 200,000 characters. That is far more than you need: only the variety of shapes matters, not the number of records. Ten representative objects produce the same types as ten thousand, as long as the ten cover the optional fields and the type variations. If anything, a smaller hand-picked sample gives better results than a raw dump, because you can make sure the edge cases are present.

Can an AI agent call this directly?

Yes, and it is built for that. There is a JSON API at /api/v1/convert, an MCP server at /api/mcp exposing a json_to_types tool, an OpenAPI document at /.well-known/openapi.json, and a machine-readable index at /llms.txt. Every page is also available as markdown at the same URL with an Accept: text/markdown header. The free allowance is 250 calls per day per caller.