Here is a function that respects the built-in JSON.stringify() rules while also limiting depth. This version handles cyclical references by making them either null, or using an optional callback to get an object ID (such as a GUID).
function stringify(val, depth, replacer, space, onGetObjID) {
depth = isNaN(+depth) ? 1 : depth;
var recursMap = new WeakMap();
function _build(val, depth, o, a, r) { // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
return !val || typeof val != 'object' ? val
: (r = recursMap.has(val), recursMap.set(val,true), a = Array.isArray(val),
r ? (o=onGetObjID&&onGetObjID(val)||null) : JSON.stringify(val, function(k,v){ if (a || depth > 0) { if (replacer) v=replacer(k,v); if (!k) return (a=Array.isArray(v),val=v); !o && (o=a?[]:{}); o[k] = _build(v, a?depth:depth-1); } }),
o===void 0 ? (a?[]:{}) : o);
}
return JSON.stringify(_build(val, depth), null, space);
}
var o = {id:'SOMEGUID',t:true};
var value={a:[12,2,{y:3,z:{o1:o}}],s:'!',b:{x:1,o2:o,o3:o}};
console.log(stringify(value, 0, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
console.log(stringify(value, 1, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
console.log(stringify(value, 2, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
console.log(stringify(value, 3, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2));
console.log(stringify(value, 4, (k,v)=>{console.log('key:'+k+';val:',v); return v}, 2, (v)=>{return v.id}));
{}
{
"a": [
12,
2,
{}
],
"s": "!",
"b": {}
}
{
"a": [
12,
2,
{
"y": 3,
"z": {}
}
],
"s": "!",
"b": {
"x": 1,
"o2": {},
"o3": null
}
}
{
"a": [
12,
2,
{
"y": 3,
"z": {
"o1": {}
}
}
],
"s": "!",
"b": {
"x": 1,
"o2": null,
"o3": null
}
}
{
"a": [
12,
2,
{
"y": 3,
"z": {
"o1": {
"id": "SOMEGUID",
"t": true
}
}
}
],
"s": "!",
"b": {
"x": 1,
"o2": "SOMEGUID",
"o3": "SOMEGUID"
}
(taken from my post here https://stackoverflow.com/a/57193068/1236397)
Here is a TypeScript version:
/** A more powerful version of the built-in JSON.stringify() function that uses the same function to respect the
* built-in rules while also limiting depth and supporting cyclical references.
*/
export function stringify(val: any, depth: number, replacer: (this: any, key: string, value: any) => any, space?: string | number, onGetObjID?: (val: object) => string): string {
depth = isNaN(+depth) ? 1 : depth;
var recursMap = new WeakMap();
function _build(val: any, depth: number, o?: any, a?: boolean, r?: boolean) {
return !val || typeof val != 'object' ? val
: (r = recursMap.has(val),
recursMap.set(val, true),
a = Array.isArray(val),
r ? (o = onGetObjID && onGetObjID(val) || null) : JSON.stringify(val, function (k, v) { if (a || depth > 0) { if (replacer) v = replacer(k, v); if (!k) return (a = Array.isArray(v), val = v); !o && (o = a ? [] : {}); o[k] = _build(v, a ? depth : depth - 1); } }),
o === void 0 ? (a?[]:{}) : o);
}
return JSON.stringify(_build(val, depth), null, space);
}
Note: Arrays are treated like strings - an array of primitive values; thus, any nested object items are treated as the next level instead of the array object itself (much like how a string can be an array of characters, but is one entity).
Update: Fixed a bug where empty arrays rendered as empty objects.