destr vs secure-json-parse vs fast-json-parse: Safe JSON Parsing in Node.js (2026)
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.parsethrows 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
| Feature | destr | secure-json-parse | fast-json-parse |
|---|---|---|---|
| Prototype pollution | ✅ Strips | ✅ Strips/errors | ❌ |
| Constructor pollution | ✅ (safeDestr) | ✅ | ❌ |
| Non-throwing | ✅ | ❌ (throws) | ✅ (result object) |
| Primitive detection | ✅ ("42" → 42) | ❌ | ❌ |
| Reviver support | ❌ | ✅ | ❌ |
| Strict mode | ✅ | Default | N/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-parsefor free fast-json-parseis 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.