JSON.stringify deep objects
I did what I initially feared I'll have to do : I took Crockford's code and modified it for my needs. Now it builds JSON but handles
- cycles
- too deep objects
- too long arrays
- exceptions (accessors that can't legally be accessed)
In case anybody needs it, I made a GitHub repository : JSON.prune on GitHub
Here is the code :
// JSON.pruned : a function to stringify any object without overflow// example : var json = JSON.pruned({a:'e', c:[1,2,{d:{e:42, f:'deep'}}]})// two additional optional parameters :// - the maximal depth (default : 6)// - the maximal length of arrays (default : 50)// GitHub : https://github.com/Canop/JSON.prune// This is based on Douglas Crockford's code ( https://github.com/douglascrockford/JSON-js/blob/master/json2.js )(function () { 'use strict'; var DEFAULT_MAX_DEPTH = 6; var DEFAULT_ARRAY_MAX_LENGTH = 50; var seen; // Same variable used for all stringifications Date.prototype.toPrunedJSON = Date.prototype.toJSON; String.prototype.toPrunedJSON = String.prototype.toJSON; var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; function quote(string) { escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder, depthDecr, arrayMaxLength) { var i, // The loop counter. k, // The member key. v, // The member value. length, partial, value = holder[key]; if (value && typeof value === 'object' && typeof value.toPrunedJSON === 'function') { value = value.toPrunedJSON(key); } switch (typeof value) { case 'string': return quote(value); case 'number': return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': return String(value); case 'object': if (!value) { return 'null'; } if (depthDecr<=0 || seen.indexOf(value)!==-1) { return '"-pruned-"'; } seen.push(value); partial = []; if (Object.prototype.toString.apply(value) === '[object Array]') { length = Math.min(value.length, arrayMaxLength); for (i = 0; i < length; i += 1) { partial[i] = str(i, value, depthDecr-1, arrayMaxLength) || 'null'; } v = partial.length === 0 ? '[]' : '[' + partial.join(',') + ']'; return v; } for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { try { v = str(k, value, depthDecr-1, arrayMaxLength); if (v) partial.push(quote(k) + ':' + v); } catch (e) { // this try/catch due to some "Accessing selectionEnd on an input element that cannot have a selection." on Chrome } } } v = partial.length === 0 ? '{}' : '{' + partial.join(',') + '}'; return v; } } JSON.pruned = function (value, depthDecr, arrayMaxLength) { seen = []; depthDecr = depthDecr || DEFAULT_MAX_DEPTH; arrayMaxLength = arrayMaxLength || DEFAULT_ARRAY_MAX_LENGTH; return str('', {'': value}, depthDecr, arrayMaxLength); };}());
An example of what can be done :
var json = JSON.pruned(window);
Note: Contrary to the code in this answer, the GitHub repository is updated when needed (documentation, compatibility, use as module in commonjs or node, specific serializations, etc.). It's a good idea to start from the repository if you need this pruning feature.
I've revised @dystroy's answer, adding:
- Indentation for sub-properties.
- An indication of where circular references point to.
/** * Returns the JSON representation of an object. * * @param {value} object the object * @param {number} objectMaxDepth for objects, the maximum number of times to recurse into descendants * @param {number} arrayMaxLength for arrays, the maximum number of elements to enumerate * @param {string} indent the string to use for indentation * @return {string} the JSON representation */var toJSON = function(object, objectMaxDepth, arrayMaxLength, indent){ "use strict"; /** * Escapes control characters, quote characters, backslash characters and quotes the string. * * @param {string} string the string to quote * @returns {String} the quoted string */ function quote(string) { escapable.lastIndex = 0; var escaped; if (escapable.test(string)) { escaped = string.replace(escapable, function(a) { var replacement = replacements[a]; if (typeof (replacement) === "string") return replacement; // Pad the unicode representation with leading zeros, up to 4 characters. return "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); }); } else escaped = string; return "\"" + escaped + "\""; } /** * Returns the String representation of an object. * * Based on <a href="https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js">https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js</a> * * @param {string} path the fully-qualified path of value in the JSON object * @param {type} value the value of the property * @param {string} cumulativeIndent the indentation to apply at this level * @param {number} depth the current recursion depth * @return {String} the JSON representation of the object, or "null" for values that aren't valid * in JSON (e.g. infinite numbers). */ function toString(path, value, cumulativeIndent, depth) { switch (typeof (value)) { case "string": return quote(value); case "number": { // JSON numbers must be finite if (isFinite(value)) return String(value); return "null"; } case "boolean": return String(value); case "object": { if (!value) return "null"; var valueIndex = values.indexOf(value); if (valueIndex !== -1) return "Reference => " + paths[valueIndex]; values.push(value); paths.push(path); if (depth > objectMaxDepth) return "..."; // Make an array to hold the partial results of stringifying this object value. var partial = []; // Is the value an array? var i; if (Object.prototype.toString.apply(value) === "[object Array]") { // The value is an array. Stringify every element var length = Math.min(value.length, arrayMaxLength); // Whether a property has one or multiple values, they should be treated as the same // object depth. As such, we do not increment the object depth when recursing into an // array. for (i = 0; i < length; ++i) { partial[i] = toString(path + "." + i, value[i], cumulativeIndent + indent, depth, arrayMaxLength); } if (i < value.length) { // arrayMaxLength reached partial[i] = "..."; } return "\n" + cumulativeIndent + "[" + partial.join(", ") + "\n" + cumulativeIndent + "]"; } // Otherwise, iterate through all of the keys in the object. for (var subKey in value) { if (Object.prototype.hasOwnProperty.call(value, subKey)) { var subValue; try { subValue = toString(path + "." + subKey, value[subKey], cumulativeIndent + indent, depth + 1); partial.push(quote(subKey) + ": " + subValue); } catch (e) { // this try/catch due to forbidden accessors on some objects if (e.message) subKey = e.message; else subKey = "access denied"; } } } var result = "\n" + cumulativeIndent + "{\n"; for (i = 0; i < partial.length; ++i) result += cumulativeIndent + indent + partial[i] + ",\n"; if (partial.length > 0) { // Remove trailing comma result = result.slice(0, result.length - 2) + "\n"; } result += cumulativeIndent + "}"; return result; } default: return "null"; } } if (indent === undefined) indent = " "; if (objectMaxDepth === undefined) objectMaxDepth = 0; if (arrayMaxLength === undefined) arrayMaxLength = 50; // Matches characters that must be escaped var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // The replacement characters var replacements = { "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", "\r": "\\r", "\"": "\\\"", "\\": "\\\\" }; // A list of all the objects that were seen (used to avoid recursion) var values = []; // The path of an object in the JSON object, with indexes corresponding to entries in the // "values" variable. var paths = []; return toString("root", object, "", 0);};