Skip to main content

Guide

destr vs secure-json-parse vs fast-json-parse 2026

Compare destr, secure-json-parse, and fast-json-parse for safe JSON parsing. Prototype pollution prevention, performance tradeoffs, and edge runtime support.

·PkgPulse Team·
0

TL;DR

destr (~8M weekly downloads) is the UnJS safe JSON parser — handles prototype pollution, returns fallback values instead of throwing, auto-detects non-JSON primitives. secure-json-parse (~10M weekly downloads) is Fastify's JSON parser — drops __proto__ and constructor.prototype keys to prevent prototype pollution attacks. fast-json-parse (~1M weekly downloads) is a try/catch-free JSON parser — wraps JSON.parse in a result object so you check .err instead of catching exceptions. In 2026: destr for general-purpose safe parsing, secure-json-parse for Fastify/security-focused APIs, fast-json-parse for simple error-free parsing when prototype safety is handled elsewhere.

Key Takeaways

  • destr: ~8M weekly downloads — UnJS ecosystem, prototype pollution safe, non-throwing
  • secure-json-parse: ~10M weekly downloads — Fastify core, strips dangerous keys
  • fast-json-parse: ~1M weekly downloads — result-object pattern, no try/catch
  • The problem: JSON.parse('{"__proto__":{"admin":true}}') → prototype pollution vulnerability
  • JSON.parse throws on invalid input — causes unhandled crashes in request handlers
  • destr handles both problems: strips __proto__ AND returns raw values instead of throwing

Quick Comparison

Featuredestrsecure-json-parsefast-json-parse
Weekly downloads~8M~10M~1M
Prototype pollution✅ Strips silently✅ Strips or errors
Constructor pollution✅ (safeDestr)
Non-throwing✅ Returns input as-is❌ Throws on bad JSON✅ Result object
Primitive detection"42"42
Reviver function✅ (JSON.parse compatible)
Strict mode✅ Opt-inDefault behaviorN/A
Fastify integration✅ Built-in body parser
Edge runtime support✅ (no Node APIs used)
Bundle size~1KB~2KB<1KB

The Problem

// Problem 1: Prototype pollution
const malicious = '{"__proto__":{"admin":true}}'
const obj = JSON.parse(malicious)
// obj.__proto__.admin = true → pollutes Object.prototype!

const user = {}
console.log(user.admin) // true — every object is now "admin"

// Problem 2: JSON.parse throws on invalid input
JSON.parse("not json")       // ❌ SyntaxError
JSON.parse(undefined)        // ❌ SyntaxError
JSON.parse("")               // ❌ SyntaxError
JSON.parse("true")           // true (valid — but is it what you expected?)

// In a request handler, both problems cause crashes:
app.post("/api/data", (req, res) => {
  const data = JSON.parse(req.body) // 💥 crashes if body is malformed
  // If body contains __proto__, your server is compromised
})

Prototype Pollution Explained

Prototype pollution is one of the more subtle JavaScript security vulnerabilities because it exploits the language's object model rather than a specific API bug. Every plain object in JavaScript — {}, new Object(), object literals — inherits from Object.prototype. If an attacker can modify Object.prototype, they affect every plain object in the entire process, not just the one they sent.

The attack vector with JSON is straightforward: JSON.parse materializes the parsed keys directly onto objects, including the special __proto__ key. When you parse {"__proto__": {"isAdmin": true}}, JSON.parse creates an object whose __proto__ property points to another object with isAdmin: true. JavaScript's property assignment treats __proto__ as a prototype setter, so this silently mutates Object.prototype.isAdmin = true. From that point forward, any code in the same process that checks if (obj.isAdmin) on a plain object will see true even if isAdmin was never explicitly set.

Real exploits using this vector have led to authentication bypasses, privilege escalation, and remote code execution in popular packages. The npm ecosystem has seen prototype pollution CVEs in lodash, jquery, minimist, handlebars, and dozens of other widely-used packages. The __proto__ vector is the most common, but constructor.prototype is equally dangerous:

// Constructor pollution — equally dangerous:
const obj = JSON.parse('{"constructor":{"prototype":{"isAdmin":true}}}')
// Object.prototype.isAdmin is now true — same result, different key

// Why this matters:
const freshObject = {}
console.log(freshObject.isAdmin) // true — you never set this!

// A real auth bypass pattern:
function checkPermission(user) {
  // user.role might be undefined, but thanks to pollution,
  // every plain object now has role = "admin":
  return user.role === "admin"
}
checkPermission({}) // true — bypassed!

The defense is simple but requires choosing the right parser: strip __proto__ and constructor.prototype keys before the parsed object is returned. destr does this by default. secure-json-parse can either strip them or throw an error. fast-json-parse and raw JSON.parse offer no protection.


destr

destr — safe, non-throwing JSON parser:

Basic usage

import { destr, safeDestr } from "destr"

// Safe parsing — no throws:
destr('{"name":"react","downloads":5000000}')
// → { name: "react", downloads: 5000000 }

destr("true")        // → true (auto-detects primitives)
destr("42")          // → 42
destr("null")        // → null
destr("not json")    // → "not json" (returns string as-is)
destr("")            // → "" (empty string, no throw)
destr(undefined)     // → undefined (no throw)

// Prototype pollution protection:
destr('{"__proto__":{"admin":true}}')
// → {} — __proto__ key stripped automatically

// safeDestr — even stricter, strips constructor/prototype too:
safeDestr('{"constructor":{"prototype":{"admin":true}}}')
// → {} — constructor key stripped

In API handlers

import { destr } from "destr"

// Express:
app.post("/api/webhook", (req, res) => {
  // Never throws — safe for any input:
  const payload = destr(req.body)

  if (!payload || typeof payload !== "object") {
    return res.status(400).json({ error: "Invalid JSON" })
  }

  // payload is safe — no prototype pollution
  processWebhook(payload)
  res.json({ ok: true })
})

// Hono:
app.post("/api/data", async (c) => {
  const raw = await c.req.text()
  const data = destr(raw)

  if (typeof data !== "object" || data === null) {
    return c.json({ error: "Expected JSON object" }, 400)
  }

  return c.json({ received: data })
})

Configuration

import { destr } from "destr"

// Strict mode — throw on invalid JSON (like JSON.parse but safe):
destr("not json", { strict: true })
// ❌ throws SyntaxError

// Valid JSON still works in strict mode:
destr('{"name":"react"}', { strict: true })
// → { name: "react" }

// destr is a drop-in replacement for JSON.parse in most cases:
// Before: const data = JSON.parse(body)
// After:  const data = destr(body)

How destr works

1. Checks for null/undefined/empty — returns as-is (no throw)
2. Detects primitives: "true" → true, "42" → 42, "null" → null
3. Calls JSON.parse for objects/arrays
4. Strips __proto__ keys from parsed result
5. If JSON.parse throws → returns original string (non-strict mode)

Performance:
  - ~2x slower than raw JSON.parse for valid JSON (safety checks)
  - Infinitely faster than crashing your server on bad input

secure-json-parse

secure-json-parse — Fastify's secure parser:

Basic usage

import sjson from "secure-json-parse"

// Safe parsing — strips prototype pollution:
sjson.parse('{"__proto__":{"admin":true}}')
// ❌ throws by default — SyntaxError: Object contains forbidden prototype property

// Scan mode — strip dangerous keys silently:
sjson.parse('{"__proto__":{"admin":true},"name":"react"}', null, {
  protoAction: "remove",  // Remove __proto__ keys silently
})
// → { name: "react" }

// Or error mode (default):
sjson.parse('{"__proto__":{"admin":true}}', null, {
  protoAction: "error",  // Throw on __proto__
})
// ❌ SyntaxError: Object contains forbidden prototype property

Configuration options

import sjson from "secure-json-parse"

// Full options:
const options = {
  protoAction: "remove",        // "error" | "remove" | "ignore"
  constructorAction: "remove",  // "error" | "remove" | "ignore"
}

// Handles nested prototype pollution:
sjson.parse('{"a":{"__proto__":{"admin":true}}}', null, options)
// → { a: {} }

// Constructor pollution:
sjson.parse('{"constructor":{"prototype":{"admin":true}}}', null, options)
// → {}

With Fastify

import Fastify from "fastify"

// Fastify uses secure-json-parse internally by default:
const app = Fastify({
  // These are the defaults:
  onProtoPoisoning: "error",       // Reject __proto__ payloads
  onConstructorPoisoning: "error", // Reject constructor payloads
})

// Your routes are automatically protected:
app.post("/api/data", async (request, reply) => {
  // request.body is already safely parsed by secure-json-parse
  // If payload contained __proto__, Fastify returns 400 automatically
  return { received: request.body }
})

// Change behavior if needed:
const app2 = Fastify({
  onProtoPoisoning: "remove",       // Strip instead of reject
  onConstructorPoisoning: "remove",
})

Standalone usage

import sjson from "secure-json-parse"

// Drop-in replacement for JSON.parse:
// Before: JSON.parse(body)
// After:  sjson.parse(body, null, { protoAction: "remove" })

// Reviver support (same as JSON.parse):
sjson.parse('{"date":"2026-03-09"}', (key, value) => {
  if (key === "date") return new Date(value)
  return value
})
// → { date: Date object }

// scan() — check if object has dangerous properties:
const parsed = JSON.parse(body)
sjson.scan(parsed, { protoAction: "remove" })
// Mutates parsed to remove __proto__ keys

fast-json-parse

fast-json-parse — result-object parser:

Basic usage

import Parse from "fast-json-parse"

// Returns result object — no try/catch needed:
const result = new Parse('{"name":"react","downloads":5000000}')

if (result.err) {
  console.error("Invalid JSON:", result.err.message)
} else {
  console.log(result.value)
  // → { name: "react", downloads: 5000000 }
}

// Invalid JSON — no throw:
const bad = new Parse("not json")
console.log(bad.err)    // SyntaxError: Unexpected token...
console.log(bad.value)  // undefined

In request handlers

import Parse from "fast-json-parse"

app.post("/api/data", (req, res) => {
  const result = new Parse(req.body)

  if (result.err) {
    return res.status(400).json({
      error: "Invalid JSON",
      details: result.err.message,
    })
  }

  // result.value is the parsed object
  processData(result.value)
  res.json({ ok: true })
})

Limitations

fast-json-parse:
  ✅ Simple — result object pattern (err/value)
  ✅ No try/catch needed — cleaner code
  ✅ Tiny — just wraps JSON.parse

  ❌ No prototype pollution protection
  ❌ No primitive detection (doesn't handle "true" → true)
  ❌ Still uses JSON.parse internally — same performance
  ❌ Mostly unnecessary now — use destr instead

In 2026:
  - fast-json-parse is largely replaced by destr
  - destr provides result-style safety PLUS prototype pollution protection
  - Or just use try/catch — it's idiomatic JavaScript

Performance Benchmarks

Raw JSON.parse is the fastest JSON parsing option in Node.js — it's a V8 built-in implemented in C++, with decades of micro-optimization work. Understanding where safe parsers add overhead helps you decide when to care.

For a typical 2KB API request body (a medium-sized JSON object), the parsing times are approximately: JSON.parse at ~0.005ms, destr at ~0.010-0.015ms, and secure-json-parse at ~0.007-0.012ms. The overhead is real but measured in microseconds. At 10,000 requests per second — a busy API — destr's overhead adds roughly 50-100ms of CPU time per second across the server. This is negligible compared to database latency, network I/O, and application logic.

The performance gap becomes relevant at two extremes: very large payloads (1MB+ JSON documents) and very high throughput bulk processing. For a 1MB payload, destr's safety scan traverses every key in the object tree, adding roughly 0.5-1ms of overhead per parse. For bulk batch operations parsing thousands of pre-validated records in a tight CPU loop — importing a dataset, processing a message queue backlog — the cumulative overhead can matter. In those specific cases, using JSON.parse directly (after validating that the data source is trusted) is justified.

For everything else — webhook handlers, REST API request bodies, configuration loading, message queue consumers receiving untrusted payloads — the performance difference between safe and unsafe parsers is not measurable in user-facing latency. Fastify's choice to ship secure-json-parse as its default body parser, despite Fastify's well-known performance focus, demonstrates that its maintainers have benchmarked this and found the tradeoff acceptable.


Edge Cases: Deeply Nested, Large Payloads, and Circular References

JSON does not support circular references — JSON.stringify throws TypeError: Converting circular structure to JSON if you try to serialize one. On the parsing side, you can't receive circular JSON because it can't be serialized. But deeply nested objects are a real concern: a maliciously crafted payload like {"a":{"a":{"a":...}}} nested 100,000 levels deep can cause a stack overflow in the parser. This is a denial-of-service vector distinct from prototype pollution. None of the three libraries protect against deep nesting by default — for that, you need a depth limit at the body parser level (most frameworks accept a depthLimit option).

For very large payloads, all three libraries parse the entire string before returning. If you're dealing with 50MB+ JSON files, consider streaming JSON parsers (jsonstream, clarinet) instead — they emit events as they parse rather than materializing the full object in memory first. destr, secure-json-parse, and fast-json-parse all require the complete JSON string as input and hold the entire parsed structure in memory.

// Depth limit with Fastify body parser:
const app = Fastify({
  ajv: {
    customOptions: { allowUnionTypes: true },
  },
})

// Or with express/body-parser:
app.use(express.json({
  limit: "1mb",  // Reject payloads over 1MB
  // Note: depth limit requires custom middleware
}))

Streaming JSON Parsing

None of the three libraries support streaming JSON parsing — they all require the complete JSON string as input. This is fine for API request bodies (the framework buffers the request before calling your handler anyway), but becomes limiting for large file processing or server-sent data where you'd prefer to process records as they arrive.

For streaming JSON, the JavaScript ecosystem offers clarinet (SAX-style events), jsonstream (transform stream with JSONPath selectors), and stream-json (the most feature-complete option). These are fundamentally different tools than destr/secure-json-parse — they trade the convenience of a complete parsed object for memory efficiency on large inputs. If you're processing large JSON files in Node.js, stream-json with StreamArray is the standard recommendation in 2026.


Edge Runtime Considerations

All three libraries work in edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) because they use only standard JavaScript — no Node.js-specific APIs like Buffer, fs, or process. This is a meaningful advantage over some older JSON utilities that depended on Node.js built-ins.

For Cloudflare Workers specifically, where bundle size affects cold start time, the sub-2KB footprint of all three libraries matters. destr at ~1KB gzipped is the lightest. The runtime environment already provides native JSON.parse, so these libraries add only the safety layer on top with minimal overhead.

// Cloudflare Worker with destr:
export default {
  async fetch(request: Request, env: Env) {
    const body = await request.text()
    const data = destr(body)  // Works identically in CF Workers

    if (!data || typeof data !== "object") {
      return new Response("Invalid JSON", { status: 400 })
    }

    return Response.json({ received: true })
  },
}

One edge runtime consideration: Deno and Bun's native JSON.parse implementations may behave slightly differently from V8's in edge cases (Unicode handling, number precision at the extremes). Testing your parsing code against your target runtime is worthwhile if you're building for multiple environments.


Schema Validation Pairing

Safe JSON parsing solves one problem — preventing crashes and prototype pollution — but it doesn't validate the shape of the data you received. A payment handler that receives {"amount": "delete * from users"} might parse safely but still cause damage downstream. The correct layering is: safe parse first, schema validate second.

destr pairs naturally with Zod in the UnJS/Nuxt ecosystem. The pattern is idiomatic: destr handles the string-to-object conversion safely, Zod handles the type narrowing and validation, and the result is both safe and properly typed:

import { destr } from "destr"
import { z } from "zod"

const PackageSchema = z.object({
  name: z.string().min(1).max(214),
  version: z.string().regex(/^\d+\.\d+\.\d+/),
  description: z.string().optional(),
})

app.post("/api/packages", async (req, res) => {
  // Step 1: Safe parse (no throws, no pollution)
  const raw = destr(req.body)

  // Step 2: Schema validation (type-safe, structured errors)
  const result = PackageSchema.safeParse(raw)
  if (!result.success) {
    return res.status(422).json({ errors: result.error.flatten() })
  }

  // result.data is fully typed: { name: string, version: string, ... }
  await createPackage(result.data)
  res.status(201).json({ ok: true })
})

Fastify's built-in approach achieves the same goal differently: secure-json-parse handles body parsing safely, and Fastify's JSON Schema validation (compiled via AJV) validates the structure before the route handler runs. The result is equivalent — safe parsing + schema validation — with the validation happening at the framework level rather than in your handler code.


Error-Throwing vs Non-Throwing Style

The choice between destr's non-throwing style and secure-json-parse's throwing style reflects a broader design philosophy question: should errors be exceptional (thrown) or ordinary (returned)?

Throwing style (secure-json-parse default, JSON.parse, destr with { strict: true }): Parse errors are exceptional — you write happy-path code and let exceptions bubble up to your error handler. This is idiomatic JavaScript and works well when your error handler is well-configured to return proper HTTP responses. The risk is forgetting the error handler exists, or having it return generic 500s instead of the 400 that bad input deserves.

Non-throwing style (destr default, fast-json-parse): Parse results are always values — you check for errors explicitly at the call site. This is idiomatic in Go, Rust, and functional languages, and makes error handling impossible to forget. The downside is verbosity: every call site needs an explicit check.

For API request handling, the non-throwing style is generally safer because it makes it obvious that you're handling bad input. Receiving malformed JSON from a client is not exceptional — it happens regularly in production. Treating it as an exception (thrown, caught by a catch-all handler) can lead to generic 500 responses for client errors that should be 400s. Treating it as an ordinary error case (checked result) puts the response decision close to the call site where context is available.

The practical recommendation: use destr in non-strict mode for anything handling untrusted external input, and use { strict: true } (or raw JSON.parse) only for trusted internal data where you genuinely want an unrecoverable error on parse failure.


JSON Parsing Security in 2026

Prototype pollution has been the root cause of several high-severity CVEs in major npm packages. The {"__proto__": {"isAdmin": true}} attack pattern is well-documented, widely known, and still actively exploited in APIs that use raw JSON.parse on untrusted input. Adding destr or secure-json-parse as a drop-in replacement in your request handling layer is one of the highest return-on-investment security improvements for Express APIs — a one-line change with permanent, broad protection.

Fastify's decision to ship secure-json-parse as its default body parser reflects the same reasoning: the cost is negligible, the protection is meaningful, and there's no reason to ship without it. For Express users who explicitly configure express.json(), the equivalent is replacing it with a body parser that uses destr or secure-json-parse.

Non-Throwing JSON Parsing for Robust APIs

Beyond prototype pollution, the most common production incident with JSON parsing is the unhandled SyntaxError from JSON.parse on malformed input. Webhook endpoints, message queue consumers, and third-party API integrations all receive data from systems outside your control. A malformed body — undefined, an empty string, truncated JSON — throws a SyntaxError that, if uncaught, becomes an unhandled exception and typically returns a 500 instead of the correct 400.

destr eliminates the need for try/catch entirely: it returns a safe value for any input and never throws in its default mode. Your validation logic then handles the malformed data gracefully. fast-json-parse's result object pattern achieves the same goal with explicit .err checking. Both prevent the "crash on bad input" failure mode — destr's implicit approach requires no code changes at the call site, while fast-json-parse makes error handling visible but requires refactoring every call site.


JSON Parsing Security in 2026

JavaScript's prototype inheritance means every object inherits properties from Object.prototype. When an attacker sends {"__proto__": {"isAdmin": true}} and your server parses it with JSON.parse, the resulting object's prototype is mutated — which means every plain object in your server process suddenly has isAdmin on its prototype chain. A check like if (req.user.isAdmin) that used to be false for unauthenticated requests can now return true even for objects that were never assigned admin privileges. This is prototype pollution, and it has been the root cause of several high-severity CVEs in major npm packages over the years.

The fix is simple but easy to overlook: use a JSON parser that strips __proto__ and constructor.prototype keys before materializing the parsed object. In 2026, prototype pollution is a well-documented problem with documented mitigations, yet many Express APIs still use raw JSON.parse on untrusted body data because the vulnerability isn't obvious from reading the code. The parser call looks safe; the danger is in JavaScript's object model, not in the call site.

Adding destr or secure-json-parse as a drop-in replacement for JSON.parse in your request handling layer is one of the highest return-on-investment security improvements for Express APIs. The effort is a one-line change. The protection is permanent — every route that processes body data benefits without any further code changes. Fastify's choice to ship secure-json-parse as its built-in body parser by default reflects the same reasoning: the cost is negligible, the protection is meaningful, and there's no reason to ship without it.

Non-Throwing JSON Parsing for Robust APIs

Beyond prototype pollution, the most common production incident with JSON parsing is the unhandled SyntaxError from JSON.parse on malformed input. Webhook endpoints, message queue consumers, and third-party API integrations all receive data from systems outside your control. A malformed body — undefined, an empty string, truncated JSON, a plain string where an object was expected — throws a SyntaxError that, if uncaught, becomes an unhandled exception. Depending on your error handling setup, this either crashes the process or returns a 500 to the client when a 400 would have been more appropriate.

The traditional solution is wrapping JSON.parse in a try/catch block, which works but is easy to forget in new code and creates inconsistent error handling patterns across a codebase. destr eliminates the need entirely: it returns a safe value for any input and never throws in its default mode. Pass it undefined, an empty string, or a truncated JSON fragment — it returns the input as-is rather than throwing. Your validation logic (Zod, Joi, or manual checks) then handles the malformed data gracefully and returns a proper 400 response.

fast-json-parse takes a result-object approach that's explicit rather than implicit: you check .err before accessing .value. This pattern is familiar to Go developers and makes error handling visible at the call site. Both patterns prevent the "crash on bad input" failure mode. destr's implicit non-throwing behavior requires no code changes from the call site — it's a true drop-in for JSON.parse. fast-json-parse's result object pattern requires refactoring every call site to check .err, which is more work but makes error handling impossible to forget.

Performance Trade-offs: Safety vs Speed

Raw JSON.parse is the fastest JSON parsing option available in Node.js — it's a built-in V8 operation optimized at the C++ level, with years of micro-optimization work. destr adds roughly 2-3x overhead for valid JSON because it scans the parsed result for prototype-polluting keys and performs the primitive detection logic that converts "true" to true and "42" to 42. For most APIs, this overhead is irrelevant: parsing a 10KB request body takes roughly 0.05ms with JSON.parse and 0.1-0.15ms with destr — both are negligible compared to network latency, database round-trips, and application logic.

Fastify's production use of secure-json-parse as its default body parser demonstrates that the security benefit justifies the minor overhead even at high throughput. Fastify is known for performance, and its maintainers have benchmarked extensively; they ship secure-json-parse by default because the throughput impact doesn't matter at realistic API request sizes.

The only scenario where raw JSON.parse's speed advantage is meaningfully significant is bulk batch processing: parsing thousands of pre-validated JSON records in a tight loop, such as importing a large dataset or processing a message queue backlog. For that pattern — where you've validated the data source as trusted and the volume is high enough for microseconds to compound — use JSON.parse directly. For every other API use case involving untrusted input from clients, webhooks, or external services, the safety of destr or secure-json-parse is worth the marginal overhead.


Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on destr v2.x, secure-json-parse v2.x, and fast-json-parse v1.x.

Compare JSON utilities and developer tooling on PkgPulse →

See also: pm2 vs node:cluster vs tsx watch and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js, supertest vs fastify.inject vs hono/testing.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.