47

When stringifying an object using JSON.stringify (or something similar) is there a way to limit the stringification depth, i.e. only go n levels deep into the object tree and ignore everything that comes after that (or better: put placeholders in there, indicating something was left out)?

I know that JSON.stringify takes a replacer function of the form function (key, value) but I didn't find a way to get the depth in the original object of the current key-value-pair handed to the replacer function.

Is there a way to do this with the default JSON.stringify implementation? Or have I reached a point where I should just implement the stringification myself? Or is there another stringification library you can recommend that has this option?

Joachim Kurz
  • 2,875
  • 6
  • 23
  • 43
  • 1
    Looks like this question's accepted answer might be helpful: http://stackoverflow.com/questions/13861254/json-stringify-deep-objects – Strille May 09 '13 at 16:10
  • @Strille Hm, it does contain an implementation that does what I want. I take it, that means it is not possible with the default implementation? I'd rather use the default implementation, since I assume it's faster since it's implemented natively. But thanks for the pointer! – Joachim Kurz May 09 '13 at 16:20
  • No, I'm pretty sure the default implementation cannot do what you ask unfortunately. – Strille May 09 '13 at 16:21

3 Answers3

15

I wanted to stringify an object on the first level without including a third-party library / too much code.

In case you look for the same, here is a quick one-liner to do so:

var json = JSON.stringify(obj, function (k, v) { return k && v && typeof v !== "number" ? (Array.isArray(v) ? "[object Array]" : "" + v) : v; });

The top level object will have no key, so it's always simply returned, however anything that is not a "number" on the next level will be casted to a string incl. a special case for arrays, otherwise those would be exposed further more.

If you don't like this special array case, please use my old solution, that I improved, too:

var json = JSON.stringify(obj, function (k, v) { return k && v && typeof v !== "number" ? "" + v : v; }); // will expose arrays as strings.

Passing an array instead of an object in the top level will work nonetheless in both solutions.

EXAMPLES:

var obj = {
  keyA: "test",
  keyB: undefined,
  keyC: 42,
  keyD: [12, "test123", undefined]
}
obj.keyD.push(obj);
obj.keyE = obj;

var arr = [12, "test123", undefined];
arr.push(arr);

var f = function (k, v) { return k && v && typeof v !== "number" ? (Array.isArray(v) ? "[object Array]" : "" + v) : v; };
var f2 = function (k, v) { return k && v && typeof v !== "number" ? "" + v : v; };

console.log("object:", JSON.stringify(obj, f));
console.log("array:", JSON.stringify(arr, f));
console.log("");
console.log("with array string cast, so the array gets exposed:");
console.log("object:", JSON.stringify(obj, f2));
console.log("array:", JSON.stringify(arr, f2));
Martin Braun
  • 10,906
  • 9
  • 64
  • 105
  • Doesn't handle arrays properly. – James Wilkins Jul 24 '19 at 20:58
  • @JamesWilkins Arrays would be cased to a string, making them readable, beyond the depth limit. I updated my answer to show `[object Array]` for not exposing arrays into the 2nd level, but preserved my old solution. I also fixed the issue that numbers will be casted to strings and I don't cast undefined's at all, so they will be ignored. Passing an array as top level object should've been worked nonetheless beforehand. – Martin Braun Jul 26 '19 at 13:34
  • Sorry, I wasn’t clear enough. I meant I consider arrays with primitive values to be the same level as strings, which are like arrays of character (strings). Converting an array to a string or “object Array” is not a correct value in my books (so to speak). { a: [1,2,3], b: 1, c: “123” } is all one level. – James Wilkins Jul 27 '19 at 18:36
  • In other languages like C, C++, C#, etc., you can more easily see that an array of integers is not much different than an array of characters. In both cases neither are objects. That said, a JavaScript array happens to be an object, but I still do not think it should be considered as one in limiting depth. – James Wilkins Jul 27 '19 at 18:53
  • @JamesWilkins so what is the expected output for `obj.keyD` from my example, literally? `"keyD":[12,"test123",,[object Object]]`? It's not easy to come to a logical perfect solution, because there are situations that will cause problems, nonetheless. Like what if I have an array in an array starting from the first level? An optimal solution would not become a one liner, I believe. So this is only aimed for serialization without the need of deserialization, later on. In my situation I came up with this solution to store some objects for later inspection. – Martin Braun Jul 30 '19 at 13:03
  • Correct, I think the level should include the array primitive items. It would also serve to be consistent with other answers of the same involving Newtonsoft.Json (https://stackoverflow.com/a/10454062/1236397). It includes the array items. Just because an `Array` happens to be an object is not a good reason to not be consistent with expectations within other languages (that is, where one would not treat an array like an object). – James Wilkins Jul 30 '19 at 22:55
8

Here is a function that respects the built-in JSON.stringify() rules while also limiting depth:

function stringify(val, depth, replacer, space) {
    depth = isNaN(+depth) ? 1 : depth;
    function _build(key, val, depth, o, a) { // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
        return !val || typeof val != 'object' ? val : (a=Array.isArray(val), 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(k, v, a?depth:depth-1); } }), o||(a?[]:{}));
    }
    return JSON.stringify(_build('', val, depth), null, space);
}

How it works:

  1. _build() is called recursively to build the nested objects and arrays to the requested depth. JSON.stringify() is used to iterate over each object's immediate properties to respect the built-in rules. 'undefined' is always returned from the internal replacer so no JSON is actually constructed yet. Keep in mind, the first time the internal replacer is called the key is empty (which is the item to be stringified).
  2. JSON.stringify() is called on the final result to produce the actual JSON.

Example:

var value={a:[12,2,{y:3,z:{q:1}}],s:'!',o:{x:1,o2:{y:1}}};

console.log(stringify(value, 0, null, 2));
console.log(stringify(value, 1, null, 2));
console.log(stringify(value, 2, null, 2));

{}

{
  "a": [
    12,
    2,
    {}
  ],
  "s": "!",
  "o": {}
}

{
  "a": [
    12,
    2,
    {
      "y": 3,
      "z": {}
    }
  ],
  "s": "!",
  "o": {
    "x": 1,
    "o2": {}
  }
}

(for a version that handles cyclical references, see here: https://stackoverflow.com/a/57193345/1236397 - includes a TypeScript version)

Update: Fixed a bug where empty arrays rendered as empty objects.

James Wilkins
  • 6,836
  • 3
  • 48
  • 73
  • printing blank for each array entry isn't very nice – Robert Oct 18 '19 at 20:37
  • Not sure what you mean. It doesn't print blank for arrays at all, I just checked again. – James Wilkins Oct 24 '19 at 20:19
  • Imagine "a" contained all objects (instead of 12 and 2 and an object). If "a" had 500 such objects you would see 500 empty brackets printed out (for a depth of 1). Not very informative. – Robert Oct 25 '19 at 15:29
  • 1
    That's by design. The array elements are depth 1, not the objects they contain. Since primitive values are not considered nested objects those are included. Any objects in an array cannot be output with properties set to undefined - that is your own custom requirement. If you want objects with properties then increase your depth level to include those properties and their primitive values. Everything works as intended. – James Wilkins Oct 28 '19 at 19:12
  • For the record, C# does the SAME thing with empty objects when using Newtonsoft.Json. When there is an empty object, it outputs `{ }`. That library doesn't support depth limits during serialization, but if it did, most likely you would get the same thing, or null). In JS, it is very easy to detect empty objects if need be. If you want null instead, then change this `a?[]:{}` to this `a?[]:null`. – James Wilkins Oct 28 '19 at 19:26
1

Make a deep clone of your object (with a library such as lodash), do what ever pruning you want to do and then pass it to JSON.stringify. I would not try to re-invent JSON.stringify, that is effort in the wrong place.

[EDIT] looks like someone already did what you were suggesting: JSON.stringify deep objects

I wouldn't recommend this though because the native JSON.stringify is always going to be faster and more robust

[EDIT] here is a library that seems to do what you want: http://philogb.github.io/jit/static/v20/Docs/files/Core/Core-js.html#$jit.json.prune

Holf
  • 5,605
  • 3
  • 42
  • 63
Jack Allan
  • 14,554
  • 11
  • 45
  • 57