Skip to main content

destr vs secure-json-parse vs fast-json-parse: Safe JSON Parsing in Node.js (2026)

·PkgPulse Team

TL;DR

destr is the UnJS safe JSON parser — handles prototype pollution, returns fallback values instead of throwing, auto-detects non-JSON primitives. secure-json-parse is Fastify's JSON parser — drops __proto__ and constructor.prototype keys to prevent prototype pollution attacks. fast-json-parse 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 (especially with untrusted input), secure-json-parse for Fastify/security-focused APIs, fast-json-parse for simple error-free parsing.

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

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
})

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

Feature Comparison

Featuredestrsecure-json-parsefast-json-parse
Prototype pollution✅ Strips✅ Strips/errors
Constructor pollution✅ (safeDestr)
Non-throwing❌ (throws)✅ (result object)
Primitive detection✅ ("42" → 42)
Reviver support
Strict modeDefaultN/A
Fastify integration✅ (built-in)
UnJS ecosystem
Weekly downloads~8M~10M~1M

When to Use Each

Use destr if:

  • Need safe JSON parsing that never throws
  • Handling untrusted user input (webhooks, APIs, forms)
  • Want primitive auto-detection ("true" → true, "42" → 42)
  • In the UnJS ecosystem (Nuxt, Nitro, H3)

Use secure-json-parse if:

  • Using Fastify (it's already included)
  • Need prototype pollution protection with JSON.parse-compatible API
  • Want to throw on dangerous payloads (strict security)
  • Need reviver function support

Use fast-json-parse if:

  • Want result-object pattern (err/value) instead of try/catch
  • Don't need prototype pollution protection
  • Simple projects where destr is overkill

For new projects in 2026:

  • Default to destr — safest, most convenient
  • Fastify users get secure-json-parse for free
  • fast-json-parse is largely obsolete — use destr instead

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.