Safe Json
Hardened JSON parse + stringify + schema validation. Native JSON.parse leaves four footguns to the caller — no size cap (DoS the parser thread), no depth cap (stack-overflow downstream), no guard on __proto__ / constructor / prototype keys (prototype pollution after any later merge/clone), and errors that report only a character offset with no surrounding context. b.safeJson closes all four with conservative defaults.
Defaults: 1 MiB body cap, depth 100, 10 000 keys per object (CVE-2026-21717 V8 HashDoS guard), poisoned keys stripped. Stringify refuses circular references unless the caller asks for the [Circular] placeholder. canonical produces RFC 8785 JCS key-sorted output for signature inputs.
The validator is a strict subset of JSON Schema (type / enum / minLength etc. / required / properties / additionalProperties), pluggable formats via b.safeJson.registerFormat, two modes: throw on first error (trust-boundary parse) or collect every error (form-style bulk validation).
Validation policy: opts and inputs are validated at the call site and throw SafeJsonError. The throw IS the security signal; HTTP middleware catches it and emits 400 with .code / .path.
b.safeJson.SafeJsonError #
Error class thrown by every b.safeJson primitive on bad input, cap exceedance, or schema-validation failure. Extends FrameworkError. Carries a stable .code (e.g. json/too-large, json/syntax, json/validation, json/circular) plus an optional JSON-pointer-shaped .path (e.g. $.user.email) for schema-validation errors. HTTP middleware translates these into 400 responses without leaking parser internals.
var b = require("blamejs");
try {
b.safeJson.parse("{not json");
} catch (e) {
e instanceof b.safeJson.SafeJsonError; // → true
e.code; // → "json/syntax"
}
b.safeJson.parse(input, opts?) #
{
maxBytes: number, // default 1 MiB; capped at 64 MiB
maxDepth: number, // default 100; capped at 1000
maxKeys: number, // default 10 000; capped at 1 000 000
allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
schema: object, // optional JSON-Schema subset; runs b.safeJson.validate
collectErrors: boolean, // pair with `schema`: return { ok, value, errors[] } instead of throwing
expectType: string, // legacy: "string"|"number"|"boolean"|"null"|"array"|"object"
requiredKeys: string[],// legacy: required top-level keys (prefer `schema.required`)
}
Hardened JSON parse. Accepts string / Buffer / Uint8Array, normalizes to UTF-8 text, enforces the byte cap BEFORE the parser sees the input, then bounds nesting depth and per-object key count so a hostile body can't DoS the parse thread or trip V8's HashDoS shape-cache degeneracy (CVE-2026-21717). Strips __proto__ / constructor / prototype keys via the JSON.parse reviver so a later spread / merge / clone can't pivot into prototype pollution.
Throws SafeJsonError with a documented .code: json/too-large / json/syntax / json/too-deep / json/too-many-keys / json/wrong-input-type / json/type-mismatch / json/missing-key / json/validation.
var b = require("blamejs");
var obj = b.safeJson.parse('{"name":"alice","age":30}');
obj.name;
// → "alice"
// Prototype-pollution payload: poisoned keys stripped silently.
var clean = b.safeJson.parse('{"__proto__":{"isAdmin":true},"id":1}');
Object.prototype.hasOwnProperty.call(clean, "__proto__");
// → false
// Size cap rejects oversized input before parsing.
var big = '"' + "x".repeat(2000) + '"';
try { b.safeJson.parse(big, { maxBytes: 1024 }); }
catch (e) { e.code; }
// → "json/too-large"
// Depth cap bounds nesting.
try { b.safeJson.parse('[[[[[[1]]]]]]', { maxDepth: 3 }); }
catch (e) { e.code; }
// → "json/too-deep"
b.safeJson.parseOrDefault(input, fallback, opts?) #
{
maxBytes: number, // default 1 MiB; capped at 64 MiB
maxDepth: number, // default 100; capped at 1000
maxKeys: number, // default 10 000; capped at 1 000 000
allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
schema: object, // optional JSON-Schema subset (see b.safeJson.validate)
}
Best-effort parse: returns fallback on any failure (size cap, syntax error, depth/key cap, schema mismatch). Useful for cache thaw / config files / optional metadata where a malformed payload shouldn't crash the caller. Same caps and prototype-pollution defense as parse.
var b = require("blamejs");
b.safeJson.parseOrDefault('{"x":1}', {});
// → { x: 1 }
b.safeJson.parseOrDefault("{not json", { x: 0 });
// → { x: 0 }
b.safeJson.parseOrDefault(null, []);
// → []
b.safeJson.parseStringOrObject(input, opts?) #
{
maxBytes: number, // forwarded to parse (default 1 MiB; capped 64 MiB)
maxDepth: number, // forwarded to parse
maxKeys: number, // forwarded to parse
errorClass: function, // typed error class to throw (else SafeJsonError)
jsonCode: string, // error code for invalid JSON (used with errorClass)
inputCode: string, // error code for a non-string/non-object input
label: string, // message prefix (default "safeJson.parseStringOrObject")
}
Accept EITHER a JSON string — parsed through parse, so the proto-pollution-key strip, depth/key caps, and size cap all apply — OR an already-decoded plain object (returned unchanged). This is the recurring "operator hands me a document as a JSON string or a pre-built object" surface (b.openapi / b.asyncapi). Routing it here means a raw JSON.parse on operator input — which keeps a "__proto__" member as an own key and imposes no size bound — cannot be hand-rolled per consumer. The divergence each consumer needs (its typed error class + codes + a generous document size cap) is carried as data, so there is no per-consumer branch.
var doc = b.safeJson.parseStringOrObject(input, {
maxBytes: C.BYTES.mib(16), errorClass: OpenApiError,
jsonCode: "openapi/bad-json", inputCode: "openapi/bad-input",
label: "openapi.parse",
});
b.safeJson.stringify(value, opts?) #
{
onCircular: "throw" | "replace", // default "throw"
circularReplacement: any, // default "[Circular]" (used when onCircular === "replace")
allowProto: boolean, // default false; keep __proto__/constructor/prototype keys
indent: number | string, // forwarded to JSON.stringify
}
JSON-encode a value with two safeguards JSON.stringify doesn't provide: a documented circular-reference policy (throw, or substitute every cycle with a placeholder string) and prototype- key suppression so an object built from a tainted parse can't leak __proto__ / constructor / prototype keys back out.
Throws SafeJsonError with .code = "json/circular" when onCircular: "throw" (default) hits a cycle.
var b = require("blamejs");
b.safeJson.stringify({ a: 1, b: 2 });
// → '{"a":1,"b":2}'
// Cycles throw by default.
var cyclic = { name: "root" };
cyclic.self = cyclic;
try { b.safeJson.stringify(cyclic); }
catch (e) { e.code; }
// → "json/circular"
// Opt into placeholder-substitution.
var out = b.safeJson.stringify(cyclic, { onCircular: "replace" });
// → '{"name":"root","self":"[Circular]"}'
b.safeJson.stringifyForScript(value, opts?) #
{
indent: number | string, // forwarded to b.safeJson.stringify
allowProto:boolean, // forwarded
}
Like b.safeJson.stringify but safe to embed verbatim inside an inline element. Raw JSON.stringify does not escape <, >, or &, so a string value containing (or