Parse JSON Safely in TypeScript

JSON.parse returns any, which bypasses TypeScript's type checking. Learn how to parse JSON safely by combining runtime validation with typed parsing so you never trust raw data from a string.

7 min read

JSON.parse converts a string into a JavaScript value. In TypeScript, its return type is any. This means the compiler applies no checks to the result.

You can assign it to any variable, access any property, and call any method without a single compiler error. There is no guarantee the data actually matches your expectations.

Safe JSON parsing means wrapping the call with validation so the result has a known, trusted type before the rest of your code touches it.

The Problem with any

Because it returns any, a type annotation on the result is just a promise to the compiler, not a checked fact. Here is what the unsafe version looks like.

typescripttypescript
interface User {
  id: number;
  name: string;
}
 
const raw = '{"id": 1, "name": "Alice"}';
const user: User = JSON.parse(raw);
 
console.log(user.name.toUpperCase());

This compiles and runs fine with valid input. But if the JSON string changes, say a missing field or a number where a string is expected, the type annotation does nothing to protect you.

The compiler trusted the assertion. The runtime crash happens somewhere far from the parse call, where the actual cause is harder to trace.

The fix is to validate, not assert.

A Safe Parsing Wrapper

Start with a function type that handles the two things that can go wrong when parsing: a syntax error from malformed JSON, and data that parses but does not match the expected shape.

typescripttypescript
type ParseResult<T> = { ok: true; value: T } | { ok: false; error: string };

The function itself takes the raw string and a validation function, then returns that result type instead of throwing, so the caller decides how to react to each failure.

typescripttypescript
type Validator<T> = (value: unknown) => value is T;
 
function safeJsonParse<T>(text: string, validate: Validator<T>): ParseResult<T> {
  try {
    const parsed: unknown = JSON.parse(text);
    if (!validate(parsed)) {
      return { ok: false, error: "Data does not match expected shape" };
    }
    return { ok: true, value: parsed };
  } catch {
    return { ok: false, error: "Invalid JSON string" };
  }
}

It catches the syntax error from malformed JSON and returns a failure. If the JSON is syntactically valid, it runs the validator, and only when both steps pass does it return a success with the typed value.

Use it with any type guard. Here is the shape a product should match.

typescripttypescript
interface Product {
  sku: string;
  price: number;
}

The guard checks both fields at runtime before TypeScript will treat a value as a Product, so a JSON object missing either field is rejected instead of silently passing through.

typescripttypescript
function isProduct(value: unknown): value is Product {
  return (
    typeof value === "object" &&
    value !== null &&
    "sku" in value &&
    typeof (value as Record<string, unknown>).sku === "string" &&
    "price" in value &&
    typeof (value as Record<string, unknown>).price === "number"
  );
}

Pass the guard as the second argument to the wrapper, then branch on the result the same way every time.

typescripttypescript
const result = safeJsonParse('{"sku": "A123", "price": 29.99}', isProduct);
 
if (result.ok) {
  console.log(result.value.price);
} else {
  console.error(result.error);
}

The caller checks the ok field to know whether parsing succeeded. On failure, the error message says whether the JSON was malformed or the shape was wrong. On success, the value is a fully typed Product.

Step-by-Step Parsing Flow

Safe JSON parse flow

The diagram shows the two independent checks. A malformed string is a syntax error caught by try/catch. A valid JSON value that does not match the shape is caught by the validator instead.

Only when both checks pass do you get a typed result back.

Parsing Arrays of Objects

JSON payloads are often arrays of objects. The same pattern applies: parse to an unknown value, then validate every element.

typescripttypescript
function isProductArray(value: unknown): value is Product[] {
  return Array.isArray(value) && value.every(isProduct);
}
 
const result = safeJsonParse(
  '[{"sku": "A", "price": 10}, {"sku": "B", "price": 20}]',
  isProductArray
);
 
if (result.ok) {
  result.value.forEach((product) => {
    console.log(`${product.sku}: $${product.price}`);
  });
}

The array check confirms the top-level type, and the every call validates each element against the same guard used above.

If any element fails the validator, the whole array is rejected. This is the safe default for structured data, since partial success is rarely meaningful.

Parsing with Zod

For complex schemas, writing type guards by hand is tedious and error-prone. Zod lets you define the schema once and get both the validator and the TypeScript type.

typescripttypescript
import { z } from "zod";
 
const ProductSchema = z.object({
  sku: z.string(),
  price: z.number().positive(),
});
 
type Product = z.infer<typeof ProductSchema>;
 
function parseProductJson(raw: string): Product {
  const parsed = JSON.parse(raw);
  return ProductSchema.parse(parsed);
}

Calling parse on the schema throws a detailed error if validation fails, including which field was wrong and why. This is more informative than a hand-written guard that returns false with no explanation.

For production code, use safeParse instead to avoid throwing.

typescripttypescript
function parseProductJson(raw: string): Product | null {
  try {
    const parsed = JSON.parse(raw);
    const result = ProductSchema.safeParse(parsed);
    return result.success ? result.data : null;
  } catch {
    return null;
  }
}

safeParse returns a result object instead of throwing. Combined with the try/catch around the parse call itself, this function handles every failure path and never lets an untyped value escape.

Avoiding Common Mistakes

The most tempting shortcut is the type assertion.

typescripttypescript
const user = JSON.parse(raw) as User;

This silences the compiler but provides zero runtime safety. If the JSON does not match the User shape, the error surfaces later as a confusing runtime failure, far from the actual cause.

Another mistake is validating too late or in too many places.

typescripttypescript
const data = JSON.parse(raw);
const name = (data as any).name as string;
const age = (data as any).age as number;

Each property access is a separate assertion with no coordination. Validate the entire object once at the parsing boundary, then use the typed result everywhere.

For more on building validation into your application flow, see runtime validation in TypeScript. For the pattern used in the safe wrapper, see Result pattern in TypeScript.

Rune AI

Rune AI

Key Insights

  • JSON.parse returns any, bypassing TypeScript's type checking completely.
  • Never use as Type assertions on JSON.parse results without validation.
  • Wrap JSON.parse in try/catch to handle SyntaxError from malformed input.
  • Validate the parsed value with a type guard or schema before using it.
  • Use a library like Zod for complex schemas; write guards by hand for simple ones.
RunePowered by Rune AI

Frequently Asked Questions

Why does JSON.parse return any in TypeScript?

JSON.parse can return any valid JSON value: an object, array, string, number, boolean, or null. TypeScript cannot know at compile time what shape the parsed data will have, so it uses any as a placeholder. You must validate the result to restore type safety.

Should I use a type assertion after JSON.parse?

Avoid type assertions like as User after JSON.parse. They tell the compiler to trust you, but provide zero runtime protection. If the JSON does not match the expected shape, your code will fail in unpredictable ways later. Always validate instead.

How do I handle JSON.parse errors gracefully?

Wrap JSON.parse in a try/catch block specifically for SyntaxError. Return a result type or null on failure. Do not let the SyntaxError propagate unless a parse failure is truly unrecoverable in your context.

Conclusion

JSON.parse returns any because the compiler cannot know what is in a string at compile time. The safe path is to validate the parsed result before treating it as a known type. Whether you use Zod, a hand-written guard, or a lightweight wrapper, the principle is the same: parse, validate, then trust.