jsondecode.com logo

HomeChevronBlogChevronJSON in TypeScript: Type-Safe Parsing and Validation

Blog post

JSON in TypeScript: Type-Safe Parsing and Validation

Parse JSON safely in TypeScript with Zod, type guards, and generics. Stop using 'any' for JSON and get full type safety.

author

Shashank Jain

Author

14/06/20262 minutes 33 seconds read
JSON in TypeScript: Type-Safe Parsing and ValidationJSON in TypeScript: Type-Safe Parsing and Validation

Article

The Problem with JSON and TypeScript

TypeScript's JSON.parse returns any, giving up all type safety. Casting with as MyType is a lie — TypeScript trusts you, but the runtime data might not match at all. The right approach is runtime validation that produces a typed result.

// Dangerous — TypeScript believes you, but runtime can still crash
const data = JSON.parse(response) as User;
console.log(data.name.toUpperCase()); // Crashes if name is missing

// Safe — validate at runtime, get type from the schema
import { z } from 'zod';
const UserSchema = z.object({ name: z.string(), age: z.number() });
type User = z.infer<typeof UserSchema>;
const data: User = UserSchema.parse(JSON.parse(response)); // Throws if invalid

Zod: Runtime Validation with TypeScript Types

import { z } from 'zod';

const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(200),
  price: z.number().positive(),
  tags: z.array(z.string()).optional().default([]),
  createdAt: z.string().datetime()
});

type Product = z.infer<typeof ProductSchema>;
// Equivalent to:
// type Product = {
//   id: string;
//   name: string;
//   price: number;
//   tags: string[];
//   createdAt: string;
// }

// Safe parse — returns { success, data } instead of throwing
const result = ProductSchema.safeParse(rawJson);
if (result.success) {
  console.log(result.data.name); // Fully typed
} else {
  console.error(result.error.issues);
}

Type Guards: Manually Written

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

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as User).id === 'number' &&
    typeof (value as User).name === 'string' &&
    typeof (value as User).email === 'string'
  );
}

const parsed: unknown = JSON.parse(jsonStr);
if (isUser(parsed)) {
  console.log(parsed.name); // TypeScript knows this is User
}

Typing API Responses

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const raw: unknown = await res.json();
  return UserSchema.parse(raw); // Validates and types in one step
}

// Generic fetch wrapper
async function fetchTyped<T>(url: string, schema: z.ZodType<T>): Promise<T> {
  const res = await fetch(url);
  const raw: unknown = await res.json();
  return schema.parse(raw);
}

JSON.stringify with TypeScript

// TypeScript knows what you are serializing
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
const json: string = JSON.stringify(user);

// Custom replacer with type safety
const filtered = JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined;
  return value;
});

FAQ

Why is JSON.parse typed as any in TypeScript?

Because TypeScript cannot know what structure a string contains at compile time. The parsed value could be anything. This is a deliberate design — it reminds you to validate the result.

Should I use Zod or io-ts or Yup?

Zod is the current community standard — best TypeScript integration, active development, clean API. io-ts is more functional/category-theory oriented. Yup is older and primarily for form validation. Default to Zod for new projects.

How do I handle deeply nested JSON types in TypeScript?

Use Zod's .object().nested() or recursive schemas. For unknown-depth JSON, use the JsonValue type from the type-fest package: type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }.

What is the unknown type and why use it instead of any?

unknown is the type-safe version of any. You can assign anything to unknown, but you cannot use it without first narrowing the type. Use unknown for untyped data (JSON.parse results, API responses) instead of any.

Can I generate Zod schemas from TypeScript types?

Not automatically — types are erased at runtime. But you can generate TypeScript types from Zod schemas (z.infer), which is the recommended direction. If you need to go the other way, tools like ts-to-zod exist but have limitations.

Keep reading

Recent blogs

View all

If jsondecode.com saved you time, share it with your team

Free forever. No ads. No sign-up. Help other developers find it.