Parse JSON in TypeScript

TypeScript uses JavaScript's JSON.parse() to parse JSON strings — but TypeScript adds a critical type safety concern that JavaScript doesn't have: JSON.parse() returns any by default, which completely bypasses TypeScript's type checking. Treating a parsed value as any means TypeScript won't catch mismatches between your expected type and the actual JSON structure at compile time or at runtime. The safest pattern is to annotate the result as unknown and narrow it with a type guard or a runtime schema validator like Zod. This guide covers 4 patterns in ascending order of safety: (1) basic any cast, (2) as type assertion, (3) generic wrapper with a type guard, (4) Zod schema validation. TypeScript 5.2+ also introduces JSON.rawJSON() for lossless number handling. For a plain JavaScript reference, see JSON.parse() in JavaScript.

Validate your JSON in Jsonic before parsing it in TypeScript.

Open JSON Formatter

Why JSON.parse() returns any in TypeScript

The TypeScript standard library defines JSON.parse with the signature (text: string, reviver?: (key: string, value: any) => any): any. The return type is any — not unknown, not a generic — because the TypeScript compiler has no way to statically determine the shape of arbitrary JSON input at compile time. This is a deliberate trade-off in the standard library definition.

The problem with any is that it silently disables TypeScript's type checker for every downstream operation. Enabling "strict": true or "noImplicitAny": true in your tsconfig.json does not help here — those options prevent implicit any from untyped parameters, but JSON.parse's return type is an explicit any in the lib definition, so it slips through every compiler flag.

// tsconfig.json: "strict": true — still no protection here

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

const jsonStr = '{"id": "not-a-number", "name": 42}'; // wrong types!

// TypeScript sees the return as 'any' — NO compile-time error
const user = JSON.parse(jsonStr);

// TypeScript is completely silent about all of these:
console.log(user.id.toFixed(2));   // runtime crash: "not-a-number".toFixed is not a function
console.log(user.name.toUpperCase()); // runtime: "42".toUpperCase — actually works by accident
console.log(user.email.trim());    // runtime crash: Cannot read properties of undefined

// Even with an explicit type annotation, 'any' is assignable to everything:
const typed: User = JSON.parse(jsonStr); // TypeScript accepts this — zero runtime safety
console.log(typed.id + 1);  // "not-a-number1" — silent string concatenation

The takeaway: every property access on a value parsed from JSON.parse without further narrowing is a runtime landmine. The solution is to treat the result as unknown and verify the shape before using it, using one of the patterns described in the sections below. You can also generate TypeScript interfaces from JSON to ensure your interface definition matches your data.

Pattern 1: Type assertion with as (simple but unsafe)

The simplest way to get a typed result from JSON.parse is a type assertion using the as keyword. This is a compile-time-only annotation — it tells the TypeScript compiler "treat this value as T" without inserting any runtime check. The compiled JavaScript output is identical to plain JSON.parse(str) with no assertion at all.

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

const jsonStr = '{"id": 1, "name": "Alice", "createdAt": "2024-01-15"}';

// Type assertion — TypeScript now treats 'user' as User
const user = JSON.parse(jsonStr) as User;

console.log(user.id);        // 1 (number) ✓
console.log(user.name);      // "Alice" ✓
console.log(user.createdAt); // "2024-01-15" ✓

// ── When it silently fails ────────────────────────────────────────────────
const badJson = '{"id": "abc", "name": 42}'; // wrong field types, missing createdAt

const badUser = JSON.parse(badJson) as User;

// TypeScript is satisfied — but these are wrong at runtime:
console.log(badUser.id + 10);         // "abc10" — string + number concatenation
console.log(badUser.name.toUpperCase()); // "42" — 42 coerced to string, then .toUpperCase()
console.log(badUser.createdAt.slice(0, 4)); // TypeError: Cannot read properties of undefined

// ── Alternative: angle-bracket syntax (not allowed in .tsx files) ─────────
// const user = <User>JSON.parse(jsonStr); // same as 'as' — use 'as' instead in React projects

Use type assertions only when you fully control the JSON source and have validated it elsewhere — for example, configuration files checked into your own repository, or data round-tripped through your own serialization layer. Never use as assertions for external API responses, user-submitted data, or anything read from a database or file system at runtime.

Pattern 2: unknown type + type guard (no dependencies)

Annotating the result as unknown instead of relying on the implicit any is a significant improvement — TypeScript will refuse to let you access any property until you narrow the type with explicit checks. This pattern requires zero additional dependencies and works in any TypeScript project. The trade-off is verbosity: you must write a type guard function that manually checks every property you care about.

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

// ── Step 1: write a type guard function ──────────────────────────────────
// A type guard has the return type 'value is T' — when it returns true,
// TypeScript narrows the type of 'value' to T inside the if block.
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as { id: unknown }).id === 'number' &&
    'name' in value &&
    typeof (value as { name: unknown }).name === 'string' &&
    'email' in value &&
    typeof (value as { email: unknown }).email === 'string'
  );
}

// ── Step 2: parse as unknown, then narrow ────────────────────────────────
const jsonStr = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';

const parsed: unknown = JSON.parse(jsonStr);

if (isUser(parsed)) {
  // TypeScript knows 'parsed' is User here
  console.log(parsed.id);    // 1
  console.log(parsed.name);  // "Alice"
  console.log(parsed.email); // "alice@example.com"
} else {
  console.error('Unexpected JSON shape');
}

// ── What happens with wrong data ──────────────────────────────────────────
const badJson = '{"id": "abc", "name": 42, "email": "x@y.com"}';
const badParsed: unknown = JSON.parse(badJson);

if (isUser(badParsed)) {
  // This block is NOT entered — 'id' is a string, guard returns false
  console.log('This will not print');
} else {
  console.error('Validation failed: id is not a number'); // ← this runs
}

The limitation of hand-written type guards becomes apparent with nested or complex schemas. A type guard for a deeply nested object with 20 fields requires 40+ lines of repetitive checks. For non-trivial schemas, the generic wrapper in Pattern 3 or Zod in Pattern 4 are far more maintainable. You can also compare approaches in JSON Schema vs TypeScript to understand how structural validation differs from TypeScript's static types.

Pattern 3: Generic parse wrapper

A reusable generic wrapper function encapsulates the parse-and-validate logic into a single callable unit. Instead of repeating the JSON.parse + unknown + type guard pattern everywhere, you pass the type guard as a function argument. This also centralizes error handling — you can choose between returning a T | null sentinel value for failed parses or throwing a descriptive error.

// ── Generic wrapper returning T | null (no exceptions) ───────────────────
function parseJson<T>(
  str: string,
  guard: (value: unknown) => value is T
): T | null {
  try {
    const parsed: unknown = JSON.parse(str);
    return guard(parsed) ? parsed : null;
  } catch {
    // JSON.parse throws SyntaxError for malformed JSON
    return null;
  }
}

// ── Generic wrapper that throws on failure ────────────────────────────────
function parseJsonOrThrow<T>(
  str: string,
  guard: (value: unknown) => value is T,
  typeName = 'expected type'
): T {
  let parsed: unknown;
  try {
    parsed = JSON.parse(str);
  } catch (err) {
    throw new Error(`Invalid JSON: ${(err as SyntaxError).message}`);
  }
  if (!guard(parsed)) {
    throw new Error(`JSON does not match ${typeName}`);
  }
  return parsed;
}

// ── Reuse with any type guard ─────────────────────────────────────────────
interface Product {
  sku: string;
  price: number;
  inStock: boolean;
}

function isProduct(v: unknown): v is Product {
  return (
    typeof v === 'object' && v !== null &&
    'sku' in v && typeof (v as { sku: unknown }).sku === 'string' &&
    'price' in v && typeof (v as { price: unknown }).price === 'number' &&
    'inStock' in v && typeof (v as { inStock: unknown }).inStock === 'boolean'
  );
}

const goodJson = '{"sku": "ABC-123", "price": 29.99, "inStock": true}';
const product = parseJson(goodJson, isProduct);

if (product !== null) {
  console.log(product.sku);     // "ABC-123"
  console.log(product.price);   // 29.99
}

// Malformed JSON → null (no crash)
const bad1 = parseJson('{invalid json}', isProduct); // null
// Wrong shape → null
const bad2 = parseJson('{"sku": 42}', isProduct);    // null

// Or throw for stricter error propagation:
const p = parseJsonOrThrow(goodJson, isProduct, 'Product');
console.log(p.price); // 29.99

The generic wrapper is an excellent pattern for internal utilities, configuration loaders, and test helpers where you want runtime validation without adding a dependency. For production code that handles API responses or user data, the Zod approach (Pattern 4) provides richer error messages and dramatically less boilerplate. If you need to work with arrays of typed objects, see how to filter typed JSON arrays after parsing.

Pattern 4: Zod schema validation (recommended for production)

Zod is the most widely used runtime validation library in the TypeScript ecosystem. Its key advantage over hand-written type guards is that it generates the TypeScript type from the schema definition — you write the shape once and get both runtime validation and compile-time type inference automatically via z.infer<typeof Schema>. Zod also produces detailed, field-level error messages with the exact path that failed.

Installation

npm install zod
# or
yarn add zod
# or
pnpm add zod

Basic usage: parse() and safeParse()

import { z } from 'zod';

// ── 1. Define a flat schema ───────────────────────────────────────────────
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime().optional(),
});

// TypeScript infers the type automatically — no separate interface needed
type User = z.infer<typeof UserSchema>;
// Equivalent to: { id: number; name: string; email: string; createdAt?: string }

// ── 2. schema.parse() — throws ZodError on failure ────────────────────────
const jsonStr = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';

try {
  const user: User = UserSchema.parse(JSON.parse(jsonStr));
  console.log(user.id);    // 1
  console.log(user.email); // "alice@example.com"
} catch (err) {
  // ZodError has 'issues' array with path + message per field
  console.error(err); // ZodError: [{ path: [...], message: "..." }]
}

// ── 3. schema.safeParse() — returns result object, never throws ───────────
const result = UserSchema.safeParse(JSON.parse(jsonStr));

if (result.success) {
  // result.data is typed as User
  console.log(result.data.name); // "Alice"
} else {
  // result.error is ZodError
  for (const issue of result.error.issues) {
    console.error(`${issue.path.join('.')}: ${issue.message}`);
  }
}

// ── 4. Nested schema example ──────────────────────────────────────────────
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zip: z.string().regex(/^\d{5}$/),
});

const OrderSchema = z.object({
  orderId: z.string().uuid(),
  amount: z.number().positive(),
  currency: z.enum(['USD', 'EUR', 'GBP']),
  shippingAddress: AddressSchema,
  tags: z.array(z.string()).default([]),
});

type Order = z.infer<typeof OrderSchema>;

const orderJson = `{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 49.99,
  "currency": "USD",
  "shippingAddress": { "street": "123 Main St", "city": "Boston", "zip": "02101" }
}`;

const order = OrderSchema.parse(JSON.parse(orderJson));
console.log(order.shippingAddress.city); // "Boston"
console.log(order.tags);                 // [] (default applied)

// ── 5. JSON array with Zod ────────────────────────────────────────────────
const UsersSchema = z.array(UserSchema);
type Users = z.infer<typeof UsersSchema>; // User[]

const usersJson = '[{"id":1,"name":"Alice","email":"a@x.com"},{"id":2,"name":"Bob","email":"b@x.com"}]';
const users = UsersSchema.parse(JSON.parse(usersJson));
console.log(users.length); // 2

When a parse fails, Zod's error messages are precise: "Expected number, received string at path: id" — far more actionable than a generic "validation failed" from a hand-written guard. Use safeParse in request handlers and API routes where a thrown exception would crash the response. Use parse in initialization code (config loading, startup checks) where a hard crash is the correct behavior.

PatternRuntime safetyError detailsDependenciesBest for
as MyTypeNoneNoneNoneTrusted internal data
unknown + type guardFullBoolean onlyNoneSimple shapes, no deps allowed
Generic wrapperFullBoolean + custom messageNoneReusable utilities, CLI tools
Zod parse()FullField-level ZodErrorZodConfig loading, startup
Zod safeParse()FullField-level ZodErrorZodAPI handlers, production

You can use our JSON to TypeScript converter to quickly generate a TypeScript interface from a sample JSON payload, then use that interface as the blueprint for your Zod schema definition.

Frequently asked questions

What type does JSON.parse return in TypeScript?

JSON.parse() returns any in TypeScript — the function signature is (text: string, reviver?: (key: string, value: any) => any): any. This is by design, because TypeScript cannot know the shape of the JSON at compile time. The any return type means TypeScript will not type-check any property accesses or method calls on the result. You must either cast to a specific type (unsafe), use unknown with a type guard (safe, no deps), or validate with Zod (safe, recommended for production). Note that enabling "noImplicitAny": true does not change this — the any here is explicit in the standard lib definition.

How do I parse JSON as a specific interface type in TypeScript?

The simplest (but unsafe) way is a type assertion: const user = JSON.parse(str) as User. For runtime safety, use Zod: define const UserSchema = z.object({ id: z.number(), name: z.string() }), then const user = UserSchema.parse(JSON.parse(str)). Zod infers the TypeScript type automatically via type User = z.infer<typeof UserSchema>, so you define the shape once and get both runtime validation and compile-time type safety. Use our JSON to TypeScript converter to generate the initial interface definition quickly.

What is the safest way to parse JSON in TypeScript?

The safest pattern is Zod's safeParse: const result = UserSchema.safeParse(JSON.parse(str)). It returns { success: true, data: User } on success and { success: false, error: ZodError } on failure — no exceptions thrown. The ZodError includes error.issues with field-level paths and messages like "Expected string, received number at path: name". For zero-dependency type safety, parse as unknown and write a type guard function that checks every property.

Why should I avoid JSON.parse(str) as MyType?

The as assertion is a compile-time-only claim — it tells TypeScript "trust me, this is MyType" without any runtime check. If the actual JSON has a field with the wrong type (e.g., id is a string instead of a number), TypeScript won't catch it, and your code will have silent type mismatches that only surface as runtime errors deep in your application. Use type assertions only for data you fully control and have validated elsewhere — never for external API responses, user input, or database query results.

How do I parse a JSON array in TypeScript with type safety?

With Zod: const UsersSchema = z.array(z.object({ id: z.number(), name: z.string() })); followed by const users = UsersSchema.parse(JSON.parse(str)). The inferred type is Array<{ id: number; name: string}>. With a type guard: write function isUserArray(v: unknown): v is User[] that checks Array.isArray(v) and verifies each element with v.every(isUser). Without a library, type guards for arrays require checking every element, which becomes verbose for large schemas. See also: filter typed JSON arrays for common array operations after parsing.

How does TypeScript's unknown type help with JSON parsing?

const data: unknown = JSON.parse(str) forces TypeScript to require type narrowing before you can use any property. Unlike any, you cannot access data.id directly — TypeScript will error: "Property 'id' does not exist on type 'unknown'." You must first narrow the type with an if check (typeof data === 'object' && data !== null && 'id' in data) or a type guard function. This makes every property access explicit and auditable, eliminating the silent type-unsafety of any. It's the minimum viable improvement you can make without adding any library.

Validate your JSON

Paste your JSON into Jsonic to catch syntax errors before parsing in TypeScript.

Open JSON Formatter