Skip to content

Instantly share code, notes, and snippets.

@westc
Last active May 5, 2026 22:28
Show Gist options
  • Select an option

  • Save westc/f1bf5453b74a055bb4013f4e23da96eb to your computer and use it in GitHub Desktop.

Select an option

Save westc/f1bf5453b74a055bb4013f4e23da96eb to your computer and use it in GitHub Desktop.
sortJSON() - Converts a JS value to canonical a JSON string that will be the same every time regardless of the order in which the keys are defined in the underlying objects.
/**
* @license Copyright 2026 - Chris West - MIT Licensed
* @see https://gist.github.com/westc/f1bf5453b74a055bb4013f4e23da96eb
*
* Converts a JS value to a canonical JSON string that will be the same every
* time regardless of the order in which the keys are defined in the underlying
* objects.
* @param {any} value
* A JavaScript value, usually an object or array, to be converted.
* @param {Parameters<typeof JSON.stringify>[1]} replacer
* An array of strings and numbers that acts as an approved list for
* selecting the object properties that will be stringified.
* @param {Parameters<typeof JSON.stringify>[2]} space
* Adds indentation, white space, and line break characters to the
* return-value JSON text to make it easier to read.
* @returns {string}
* @throws {TypeError}
* If a circular reference or a BigInt value is found.
*/
function sortJSON(value, replacer, space) {
/**
* @param {any} value
* @param {any} key
* @param {WeakSet<object>} ancestors
*/
function recurse(value, key, ancestors) {
const originalValue = value;
// Make sure things like Date objects will get toJSON() called.
if (typeof value?.toJSON === 'function') {
value = value.toJSON(key);
}
if (typeof value === 'object' && value !== null) {
// Detect if this is a circular structure and then make it possible to
// detect circular structures nested further down.
if (ancestors.has(value)) throw new TypeError('Unable to convert a circular structure into JSON.');
ancestors.add(originalValue);
// If dealing with an array then simply call recurse with originalValue as
// part of ancestors.
if (Array.isArray(value)) {
value = value.map((subvalue, index) => recurse(subvalue, index + '', ancestors));
}
// If dealing with an object then get the entries, order them
// alphabetically by their corresponding keys and recreate the object so
// that JSON objects will be deterministic. Calling recurse also ensures
// that possible circular structures will be detected and makes sure that
// the same thing will happen to all nested structures.
else {
const entries = Object.entries(value).map(([subkey, subvalue]) => [subkey, recurse(subvalue, subkey, ancestors)]);
value = Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
}
// Remove the original value from the ancestors set since we are about to
// return the resulting value.
ancestors.delete(originalValue);
}
// Return the normalized value.
return value;
}
// Return the normalized value as a JSON string.
return JSON.stringify(recurse(value, '', new WeakSet()), replacer, space);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment