json-stable-stringify and json-stable-stringify-without-jsonify are both npm packages designed to provide deterministic JSON serialization by sorting object keys alphabetically before stringification. This ensures consistent output regardless of property insertion order, which is critical for generating cache keys, creating digital signatures, or comparing objects. json-stable-stringify uses the jsonify package internally to handle edge cases like circular references and non-JSON-safe values, while json-stable-stringify-without-jsonify avoids this dependency entirely, offering a lighter but less robust alternative that only works reliably with plain JSON-compatible data.
Both json-stable-stringify and json-stable-stringify-without-jsonify solve a common but subtle problem in JavaScript applications: producing consistent, predictable JSON output regardless of property insertion order. This matters when you need to compare objects, generate cache keys, or create digital signatures — situations where { a: 1, b: 2 } and { b: 2, a: 1 } must serialize identically.
However, these packages differ significantly in implementation strategy, dependencies, and compatibility. Let’s break down what each offers and where they diverge.
Native JSON.stringify() does not guarantee consistent key ordering across environments or even within the same runtime if object properties were added in different orders:
// Unreliable with native JSON.stringify
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 2, a: 1 };
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // false in some engines
Stable stringifiers sort object keys alphabetically before serialization, ensuring deterministic output:
// Both packages produce this consistently:
'{"a":1,"b":2}'
This is essential for:
The key difference lies in how each package handles circular references and complex data types.
json-stable-stringify uses the jsonify package internally to safely handle edge cases like:
// json-stable-stringify
import stableStringify from 'json-stable-stringify';
const obj = { b: 2, a: 1 };
console.log(stableStringify(obj)); // '{"a":1,"b":2}'
// Handles circular refs gracefully
const circular = { a: 1 };
circular.self = circular;
console.log(stableStringify(circular)); // Throws or handles per options
json-stable-stringify-without-jsonify avoids the jsonify dependency entirely, using a simpler recursive approach that only supports plain JSON-compatible data (objects, arrays, strings, numbers, booleans, null). It will fail or behave unpredictably with:
// json-stable-stringify-without-jsonify
import stableStringify from 'json-stable-stringify-without-jsonify';
const obj = { b: 2, a: 1 };
console.log(stableStringify(obj)); // '{"a":1,"b":2}'
// Will crash on circular refs
const circular = { a: 1 };
circular.self = circular;
// stableStringify(circular); // Maximum call stack size exceeded
json-stable-stringify pulls in jsonify as a transitive dependency. While jsonify is a small, well-maintained package, this adds an extra layer to your dependency tree. However, it ensures robust handling of non-standard inputs.
json-stable-stringify-without-jsonify has zero dependencies, making it ideal for:
Both packages support identical core APIs and options:
// Common options supported by both
stableStringify(obj, {
space: 2, // indentation
cmp: (a, b) => a.key < b.key ? -1 : 1, // custom sort
replacer: (key, val) => /* transform values */
});
However, only json-stable-stringify properly respects the replacer function for non-JSON-safe values because of its jsonify foundation. The -without-jsonify variant may ignore or mishandle replacer logic when encountering functions or undefined.
You’re building a build tool that caches transformation results based on config hashes.
json-stable-stringify-without-jsonifyimport stringify from 'json-stable-stringify-without-jsonify';
import crypto from 'crypto';
function getConfigHash(config) {
return crypto.createHash('sha256')
.update(stringify(config))
.digest('hex');
}
Your app stores complex state that might include non-serializable values during development.
json-stable-stringifyimport stableStringify from 'json-stable-stringify';
// Safely serialize even if state contains unexpected values
const snapshot = stableStringify(reduxState, {
replacer: (key, value) => {
if (typeof value === 'function') return '[Function]';
return value;
}
});
You’re signing payloads that must be strictly JSON-compliant per spec.
json-stable-stringify-without-jsonifyThe -without-jsonify variant will crash on circular references due to infinite recursion. If there’s any chance your data might contain cycles (common in tree structures, DOM-like objects, or ORM models), do not use it.
// Dangerous with json-stable-stringify-without-jsonify
const node = { id: 1 };
node.children = [{ parent: node }]; // Circular!
// This will throw: RangeError: Maximum call stack size exceeded
stringify(node);
In contrast, json-stable-stringify detects cycles and throws a clear error (or handles them if you provide a custom replacer).
| Feature | json-stable-stringify | json-stable-stringify-without-jsonify |
|---|---|---|
| Dependencies | Requires jsonify | Zero dependencies |
| Circular Reference Handling | Detects and errors gracefully | Crashes with stack overflow |
| Non-JSON Value Support | Handles via jsonify + replacer | Unpredictable behavior |
| Bundle Size | Slightly larger | Minimal |
| Input Requirements | Forgiving | Strictly JSON-safe data only |
Choose json-stable-stringify if you need robustness against malformed or complex inputs, especially in libraries or tools where you don’t control the data shape.
Choose json-stable-stringify-without-jsonify only if you have full control over input data (guaranteed JSON-safe, no cycles) and prioritize minimal dependencies.
Remember: deterministic output is useless if your app crashes during serialization. When in doubt, go with the more resilient option.
Choose json-stable-stringify-without-jsonify only when you have strict control over your input data and can guarantee it contains exclusively JSON-safe values (no functions, undefined, symbols, or circular structures). Its zero-dependency design makes it ideal for minimal bundle size requirements in applications like build tools or cryptographic signing where inputs are pre-validated and simplicity is prioritized over resilience.
Choose json-stable-stringify when you need reliable handling of complex or untrusted input data that might contain circular references, functions, or other non-JSON-safe values. Its integration with jsonify provides graceful error handling and consistent behavior across edge cases, making it suitable for libraries, debugging tools, or any scenario where input validation isn't guaranteed. The slight increase in dependency footprint is justified by the robustness it offers.
This is the same as https://github.com/substack/json-stable-stringify but it doesn't depend on libraries without licenses (jsonify).
deterministic version of JSON.stringify() so you can get a consistent hash
from stringified results
You can also pass in a custom comparison function.
var stringify = require('json-stable-stringify');
var obj = { c: 8, b: [{z:6,y:5,x:4},7], a: 3 };
console.log(stringify(obj));
output:
{"a":3,"b":[{"x":4,"y":5,"z":6},7],"c":8}
var stringify = require('json-stable-stringify')
Return a deterministic stringified string str from the object obj.
If opts is given, you can supply an opts.cmp to have a custom comparison
function for object keys. Your function opts.cmp is called with these
parameters:
opts.cmp({ key: akey, value: avalue }, { key: bkey, value: bvalue })
For example, to sort on the object key names in reverse order you could write:
var stringify = require('json-stable-stringify');
var obj = { c: 8, b: [{z:6,y:5,x:4},7], a: 3 };
var s = stringify(obj, function (a, b) {
return a.key < b.key ? 1 : -1;
});
console.log(s);
which results in the output string:
{"c":8,"b":[{"z":6,"y":5,"x":4},7],"a":3}
Or if you wanted to sort on the object values in reverse order, you could write:
var stringify = require('json-stable-stringify');
var obj = { d: 6, c: 5, b: [{z:3,y:2,x:1},9], a: 10 };
var s = stringify(obj, function (a, b) {
return a.value < b.value ? 1 : -1;
});
console.log(s);
which outputs:
{"d":6,"c":5,"b":[{"z":3,"y":2,"x":1},9],"a":10}
If you specify opts.space, it will indent the output for pretty-printing.
Valid values are strings (e.g. {space: \t}) or a number of spaces
({space: 3}).
For example:
var obj = { b: 1, a: { foo: 'bar', and: [1, 2, 3] } };
var s = stringify(obj, { space: ' ' });
console.log(s);
which outputs:
{
"a": {
"and": [
1,
2,
3
],
"foo": "bar"
},
"b": 1
}
The replacer parameter is a function opts.replacer(key, value) that behaves
the same as the replacer
from the core JSON object.
With npm do:
npm install json-stable-stringify
MIT