Last active
May 5, 2026 22:28
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @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