Skip to content
Back to Blog
developerjson-formattertypescriptweb-development

How to Convert JSON to TypeScript Types

Pasting JSON into a type generator is easy; getting optional fields and union types right is not. Here is how to do it properly, plus a free in-browser converter.

SZ
Founder, Molixa
10 min read
Share
How to Convert JSON to TypeScript Types
Table of contents7 sections

To convert JSON to TypeScript, map each JSON value to its type: strings become string, numbers become number, objects become nested interface definitions, and arrays become T[]. The hard parts are inferring optional versus required fields, collapsing mixed arrays into union types, and deciding when you also need runtime validation with Zod.

Auto-generation handles the boring 90%. The remaining 10%, the part a naive converter gets wrong, is where bugs live. A single API sample cannot tell you which fields are nullable, which are sometimes missing, or whether an empty array is string[] or never[]. This guide walks through the mechanics and the gotchas, then shows how to generate the interface, a Zod schema, and a Go struct from one paste.

The Basic Mapping: JSON Values to TypeScript Types#

Start with a concrete API response. Say you are integrating a user endpoint:

{
  "id": 4821,
  "username": "ada",
  "email": "[email protected]",
  "isActive": true,
  "roles": ["admin", "editor"],
  "profile": {
    "displayName": "Ada Lovelace",
    "avatarUrl": null
  },
  "lastLoginAt": "2026-05-30T11:04:00Z"
}

A direct, correct translation looks like this:

interface User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;
  roles: string[];
  profile: Profile;
  lastLoginAt: string;
}

interface Profile {
  displayName: string;
  avatarUrl: string | null;
}

A few decisions are already baked in here that a careless generator would miss. Notice avatarUrl is string | null, not string, because the sample value is null. Notice lastLoginAt is string, not Date, because JSON has no date type, it is just an ISO string until you parse it. And profile was extracted into its own named Profile interface instead of being inlined, which keeps the types reusable.

The core mapping table is short:

JSONTypeScriptWatch out for
"text"stringISO dates are still string
42, 3.14numberNo int vs float distinction
true / falseboolean
nullnullUsually part of a union, e.g. string | null
{ ... }interface / nested objectExtract reused shapes into named interfaces
[ ... ]T[]Mixed-type arrays become a union; empty arrays are ambiguous

Interface vs Type Alias: Which to Generate#

Both interface and type can describe an object shape, and for plain API responses they are nearly interchangeable. The practical guidance:

  • Use interface for object shapes you might extend or that represent an API contract. Interfaces support declaration merging and produce cleaner error messages.
  • Use type when you need unions, intersections, mapped types, or tuples that an interface cannot express.

For JSON-derived models, default to interface. You only reach for type when the data itself is a union, such as a field that is "pending" | "complete". Most generators let you pick the output style, and interface is the safer default for an external API shape.

The Three Gotchas Single-Sample Generators Get Wrong#

This is where most online converters fall short. They translate exactly what you paste and nothing more, so the types are only as correct as your one sample.

Gotcha 1: Optional vs required fields#

One response cannot tell you a field is optional. If avatarUrl is present in your sample but the API omits it for users who never set one, a generator marks it required and your code breaks at runtime when it is missing.

You have two honest options. Either feed the generator multiple samples so it can see which keys disappear, or mark fields optional by hand based on the API docs:

interface User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;
  roles: string[];
  profile: Profile;
  lastLoginAt?: string; // optional: absent for users who never logged in
}

The ? makes the property string | undefined. That is different from string | null. null means the key is present with a null value; undefined means the key may be absent entirely. Conflating the two is a classic source of cannot read property of undefined errors.

Gotcha 2: Union types from mixed arrays#

Real arrays are not always homogeneous. Consider a feed of events:

{
  "events": [
    { "type": "click", "x": 120, "y": 44 },
    { "type": "scroll", "delta": -300 }
  ]
}

A weak generator either picks the first element's shape and ignores the rest, or merges every field into one bloated object with everything optional. The correct output is a discriminated union:

type AppEvent =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; delta: number };

interface EventPayload {
  events: AppEvent[];
}

The shared type field is the discriminant. TypeScript can now narrow the union in a switch (event.type), which a single merged interface would never allow.

Gotcha 3: Empty arrays and null-only fields#

What type is "tags": []? There is no element to infer from, so the honest answer is "unknown." Naive tools emit any[] or never[], both wrong in practice. The same problem hits a field whose only sample value is null: you cannot know the non-null type from null alone. When you hit these, supply a sample with data, or annotate the type yourself (tags: string[]) from what you know the API returns.

When You Also Need Zod (Runtime Validation)#

TypeScript types vanish at compile time. They give you zero protection against an API that actually returns something different from what you typed. If the server sends a string where you expected a number, TypeScript already trusted the wrong shape and your app fails downstream with a confusing error.

Zod closes that gap. You define a schema once, validate the response at the boundary, and infer the static type from the same schema so you never write it twice:

import { z } from "zod";

const ProfileSchema = z.object({
  displayName: z.string(),
  avatarUrl: z.string().url().nullable(),
});

const UserSchema = z.object({
  id: z.number().int(),
  username: z.string(),
  email: z.string().email(),
  isActive: z.boolean(),
  roles: z.array(z.string()),
  profile: ProfileSchema,
  lastLoginAt: z.string().datetime().optional(),
});

// The static type is derived from the schema, not hand-written:
type User = z.infer<typeof UserSchema>;

// At the network boundary:
const user = UserSchema.parse(await response.json());

Now UserSchema.parse() throws a precise, readable error the moment the data violates the contract, and z.infer keeps the TypeScript type perfectly in sync. Use Zod (or a peer like Valibot) whenever data crosses a trust boundary: HTTP responses, form input, webhooks, message queues, anything you did not produce yourself. For internal, fully typed data you control end to end, plain interfaces are enough.

Rule of thumb: if the JSON comes from outside your codebase, validate it at runtime. Types alone are a promise, not a guarantee.

Need a Go Struct Instead?#

Backend teams often need the same shape in Go. The mapping mirrors TypeScript, with struct tags carrying the JSON key names:

type Profile struct {
	DisplayName string  `json:"displayName"`
	AvatarURL   *string `json:"avatarUrl"`
}

type User struct {
	ID          int      `json:"id"`
	Username    string   `json:"username"`
	Email       string   `json:"email"`
	IsActive    bool     `json:"isActive"`
	Roles       []string `json:"roles"`
	Profile     Profile  `json:"profile"`
	LastLoginAt string   `json:"lastLoginAt"`
}

Note the *string pointer for avatarUrl: in Go, a pointer is how you represent a nullable field, since the zero value of string is "", not nil. The same nullability question from the TypeScript side resurfaces here in a different shape.

Generate It Without Pasting Into a Random Site#

Doing this by hand for a large payload is tedious and error-prone, but pasting production JSON, which may contain tokens, emails, or customer data, into an unknown remote service is worse. A privacy-conscious converter does the transformation entirely in your browser so the data never touches a server.

Step 1: Paste and validate the JSON#

Drop your API response into a free JSON formatter and converter. It first validates the JSON and pretty-prints it, so you catch a trailing comma or unquoted key before you even think about types. Malformed JSON is the most common reason a conversion silently produces garbage.

Step 2: Pick your output target#

Choose the format you need: a TypeScript interface, a Zod schema, or a Go struct. Because the JSON to TypeScript converter runs client-side, you can safely throw real, unredacted responses at it without worrying about where the data goes. The whole transform happens in the tab.

Step 3: Refine optional fields and unions by hand#

Generation gives you a correct skeleton from the sample. Now apply the three gotchas: mark genuinely optional keys with ?, widen null-only or empty-array fields to their real types, and convert mixed object arrays into discriminated unions. This last 10% is judgment the tool cannot make from one payload, and it is what separates types that compile from types that actually match the API.

When your workflow involves more than types, the same toolkit helps: validate tokens with the JWT decoder-adjacent tools, test extraction patterns in the regex tester, or handle encoded payloads with the Base64 encoder and decoder. For a deeper look at day-to-day JSON workflows, see our guide on why a JSON formatter is a daily developer tool.

Frequently Asked Questions#

How do I convert JSON to a TypeScript interface? Map each JSON value to its TypeScript equivalent: strings to string, numbers to number, objects to nested interfaces, and arrays to T[]. Extract reused object shapes into named interfaces. The fastest reliable path is pasting the JSON into a converter, then manually fixing optional fields, null unions, and mixed arrays the sample could not reveal.

Should I use an interface or a type alias for JSON data? Default to interface for object shapes from an API, since interfaces extend cleanly and give clearer error messages. Use a type alias when the data is itself a union, intersection, or tuple that an interface cannot express, such as a status field that is "pending" | "complete".

How do I handle optional fields when generating types from JSON? A single sample cannot reveal optional fields, so a generator marks everything required. Either feed it multiple responses so it sees which keys disappear, or add the ? modifier yourself based on the API docs. Remember field?: string (may be absent) differs from field: string | null (present but null).

Do I need Zod if I already have TypeScript types? Yes, whenever data crosses a trust boundary. TypeScript types are erased at compile time and cannot validate what an API actually returns at runtime. Zod validates the real payload and lets you infer the static type from the same schema with z.infer, so the type and the runtime check never drift apart.

Is it safe to convert JSON to TypeScript online? Only if the converter runs in your browser. Many online tools send your pasted JSON to a server, which is risky when the payload contains tokens, emails, or customer data. A client-side converter like the Molixa JSON formatter does the entire transformation locally, so the data never leaves your tab.

How do I convert nested JSON objects to TypeScript? Extract each nested object into its own named interface and reference it from the parent, rather than deeply inlining everything. For the user example, profile becomes a separate Profile interface that User references. Named interfaces are reusable, produce shorter error messages, and make the generated types far easier to read.

developerjson-formattertypescriptweb-development

More from Molixa

Try Molixa Tools

50+ free AI tools for content creation, SEO, coding, and more. No signup, no watermark.

Explore all tools