Parsing JSON looks like a one-liner until the input is malformed, hostile, or larger than you planned for. This guide walks through the failure modes of

text
JSON.parse
and the patterns that turn raw text into data you can actually trust.

Why
text
JSON.parse
Needs More Than a Single Line

The canonical call is simple:

js
const data = JSON.parse(rawText);

The problem is everything that call assumes. It assumes

text
rawText
is a string, that the string is syntactically valid JSON, that the resulting shape matches what your code expects, and that the values inside are safe to use. None of those are guaranteed when the input comes from a network response, a file,
text
localStorage
, or a user.

text
JSON.parse
has exactly one error channel: it throws a
text
SyntaxError
when the text is not well-formed. It does nothing about wrong shapes.
text
JSON.parse('42')
,
text
JSON.parse('null')
, and
text
JSON.parse('"hello"')
all succeed and return a number,
text
null
, and a string respectively — even if your code expected an object. Valid JSON is not the same as valid data.

Always Wrap Parsing in try/catch

Because

text
JSON.parse
throws, an unguarded call can crash a request handler or take down a render. Wrap it and return a result you can branch on:

js
function safeParse(text) { try { return { ok: true, value: JSON.parse(text) }; } catch (err) { return { ok: false, error: err.message }; } } const result = safeParse(rawText); if (!result.ok) { // log, fall back to a default, or return a 400 — but never proceed blindly return; } const data = result.value;

Returning a tagged object instead of throwing keeps the failure visible at the call site. The caller has to acknowledge that parsing might fail, which is exactly the discipline you want around untrusted input. A bare

text
try { ... } catch {}
that swallows the error is worse than no try/catch at all — it hides the fact that you are now operating on
text
undefined
.

One practical note: guard the type before you parse.

text
JSON.parse(undefined)
coerces to the string
text
"undefined"
and throws a confusing error. A quick
text
typeof text !== 'string'
check up front gives you a clearer failure.

Reviver Functions: Transform While You Parse

text
JSON.parse
accepts a second argument, a reviver function called for every key/value pair as the tree is built, from the leaves up. It is the right place to normalize values that JSON cannot represent natively — most commonly dates, which serialize to strings.

js
const isoDate = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/; const data = JSON.parse(rawText, (key, value) => { if (typeof value === 'string' && isoDate.test(value)) { return new Date(value); } return value; });

A reviver can also strip fields you never want to carry forward. Returning

text
undefined
from the reviver deletes that key from the result:

js
const clean = JSON.parse(rawText, (key, value) => key === 'password' ? undefined : value );

Two things to keep in mind. The reviver runs bottom-up, so when it is called for the root (the empty-string key), its children have already been transformed. And a reviver is a transformer, not a validator — it can reshape values but it is awkward for asserting that the overall structure is correct. For that, validate after parsing.

Validate the Shape, Not Just the Syntax

This is the step most code skips. After a successful parse you have some JavaScript value; you do not yet know it is the value your code needs. Before you read

text
data.user.email
, confirm
text
data
is an object,
text
data.user
is an object, and
text
email
is a string. Otherwise a payload of
text
null
or
text
[]
becomes a
text
TypeError
three lines later — often somewhere far from the parse call, which makes it hard to debug.

For small, well-known shapes a hand-written guard is clear and dependency-free:

js
function isUser(value) { return ( value !== null && typeof value === 'object' && typeof value.id === 'string' && typeof value.email === 'string' && (value.age === undefined || typeof value.age === 'number') ); } const result = safeParse(rawText); if (!result.ok || !isUser(result.value)) { return; // reject: bad syntax or wrong shape } const user = result.value; // now safe to use

Note the

text
value !== null
check before
text
typeof value === 'object'
. In JavaScript
text
typeof null
is
text
'object'
, a classic trap that lets
text
null
slip through naive guards.

Schema Validation for Anything Non-Trivial

Once the shape has more than a few fields, nested objects, optional properties, enums, or arrays, hand-written guards become long and error-prone. This is where schema validation earns its place. The idea is to declare the expected structure once and let a validator both check the data and narrow its type in the same step.

The pattern, regardless of the tool, looks like this:

js
// Pseudocode of the schema-validation pattern const UserSchema = object({ id: string(), email: string().email(), age: number().int().min(0).optional(), roles: array(enumOf(['admin', 'user'])), }); const parsed = safeParse(rawText); if (!parsed.ok) return reject('invalid JSON'); const check = UserSchema.validate(parsed.value); if (!check.ok) return reject(check.errors); // structured, per-field errors const user = check.value; // validated and typed

The benefits over ad-hoc checks: schemas give you precise, per-field error messages ("

text
email
must be a string") instead of a vague boolean, they double as living documentation of your contract, and in TypeScript projects they infer the static type so your runtime check and compile-time type can never drift apart. There is a separate ecosystem standard, JSON Schema, that does the same job declaratively as data — useful when the same contract must be shared across services or languages.

The rule of thumb: validate at the boundary. Every place untrusted JSON enters your program — an API handler, a webhook, a config file, a message off a queue — should validate before the data flows inward. Code past the boundary then gets to assume clean input.

Handling Untrusted Input Safely

When JSON comes from outside your trust boundary, parsing and shape-checking are necessary but not sufficient. A few extra concerns:

  • text
    __proto__
    and prototype pollution.
    A payload like
    text
    {"__proto__": {"isAdmin": true}}
    is valid JSON.
    text
    JSON.parse
    itself does not pollute the prototype, but code that later merges parsed data into an object with a naive deep-copy can. Avoid blind recursive merges of untrusted objects; if you must copy, skip the keys
    text
    __proto__
    ,
    text
    constructor
    , and
    text
    prototype
    , or build the target with
    text
    Object.create(null)
    .
  • Never
    text
    eval
    JSON.
    text
    eval
    and
    text
    new Function
    will execute embedded code and are a remote-code-execution vector.
    text
    JSON.parse
    is the only correct tool; it cannot run code.
  • Reject before you trust. Treat a parse failure or a schema failure as a
    text
    400
    -class outcome, log enough to diagnose it, and return a generic message to the caller. Do not echo raw parser errors back to clients — they can leak internal structure.
  • Bound the size. An attacker can send a multi-megabyte body specifically to exhaust memory or CPU. Enforce a maximum content length before you read the body into a string and parse it.

If you are inspecting a suspicious or unfamiliar payload by hand, a client-side viewer that runs entirely in your browser keeps the data off third-party servers — our JSON formatter is built for exactly that kind of safe inspection and pretty-printing.

Large Payloads: Where
text
JSON.parse
Breaks Down

text
JSON.parse
is synchronous and builds the entire object tree in memory at once. For typical API responses this is fine. For very large documents it becomes a liability in two ways: it blocks the main thread (in the browser, that means a frozen UI; on a server, a stalled event loop), and it requires the whole structure to fit in memory simultaneously.

Strategies, roughly in order of how big the data is:

  • Move it off the main thread. In the browser, parse inside a Web Worker so a large
    text
    JSON.parse
    does not freeze rendering. The worker posts the result back when done.
  • Don't send one giant blob. If you control the API, paginate or page the data into smaller responses. Parsing ten 1 MB chunks is far gentler than one 10 MB parse, and it lets you process incrementally.
  • Stream it. For data measured in hundreds of megabytes — large exports, log files, line-delimited records — a streaming parser reads the text in chunks and emits values as it goes, so you never hold the full tree in memory. On a server, processing a stream of records one at a time keeps memory flat regardless of file size. JSON Lines (one JSON object per line) is a simple, robust format for this: read line by line and
    text
    JSON.parse
    each line independently, which also means one corrupt record doesn't sink the whole file.
  • Validate cheaply first. If you only need to confirm a large payload is well-formed before storing it, a fast well-formedness check is lighter than a full materialize-and-walk.

The meta-point:

text
JSON.parse
is the right default, but "parse the whole thing into memory" is an assumption worth revisiting once payloads get large.

A Sensible Default Pipeline

Putting it together, robust JSON handling at a boundary is four steps, not one:

  1. Guard the input type and bound its size before parsing.
  2. Parse inside try/catch, returning a result you must check.
  3. Validate the shape — a hand-written guard for simple data, a schema for anything real.
  4. Only then use the data, confident it is both well-formed and well-shaped.

Most JSON bugs come from collapsing these into the single optimistic line we started with. Treat parsing as a place where things go wrong, and the rest of your code gets to assume they didn't. If you want to eyeball or reformat a payload while debugging, the privacy-first JSON formatter handles it locally — no upload required.