4

Users can download JSON files from our application, which I want to prettify for easier debugging than if all is in one line. However this increases the file size by nearly 40% even if the indentation is just a single tab or space.

As a compromise, I want to exclude all values with e.g. the key "large" from prettification, like this:

{
 "small":
 {
  "a": 1,
  "b": 2,
  "large": {"a":1,"b":2,"c":3,"d":"the \"large\" object should not be prettified"} 
 },
  "large": {"a":1,"b":2,"c":3,"d":"the \"large\" object should not be prettified"}
}

I tried to solve this using the replacer parameter of JSON.stringify:

JSON.stringify(data,(key,value)=>key==="large"?JSON.stringify(data):value,'\t');

However the values for the "large" keys ends up escaped:

data = 
{
     "small":
     {
      "a": 1,
      "b": [2,3,4,5],
      "large": {"myarray":[1,2,3,4],"b":2,"c":3,"d":"the \"large\" object should not be prettified"} 
     },
      "large": {"a":1,"b":2,"c":3,"d":"the \"large\" object should not be prettified"}
    }
    
 const json =  JSON.stringify(data,(key,value)=>key==="large"?JSON.stringify(data):value,'\t');
 document.getElementById("json").innerText = json;
<html>
<body>
<pre id="json">
</pre>
</body>
</html>

How can I prevent this escaping from happening or otherwise partially prettify JSON in JavaScript?

Konrad Höffner
  • 11,100
  • 16
  • 60
  • 118
  • 2
    "*However this increases the file size by nearly 40%*" is this really a problem? The data won't go through the network, since it's all contained on the client, so it won't really take more time to travel, or eat up more bandwidth, or in any way affect the data cap of the user (if any). At most, it would take more space on the disk but unless your .json files take hundreds of megabytes *or* you produce thousands and thousands of them, I don't really think drive space should be an issue. – VLAZ Oct 01 '20 at 09:12
  • 4
    In addition to what @VLAZ said: enable gzip compression. It will help a whole lot with reducing the impact white space has on your over-the-wire file size. – RickN Oct 01 '20 at 09:16
  • 1
    @RickN yes, that would help. In fact, it's very likely in effect already - most of the times gzipping is enabled by default. However, since this data is not travelling from the server to the client, it won't be gzipped. No need to, it's *on* the client already - the user's machine will produce the data and the user would then "download" that data - basically it would be saved from the RAM to the disk. – VLAZ Oct 01 '20 at 09:19
  • 1
    I think you would have to implement stringify yourself. There are workarounds (such as replacing the key with random 5 character, then replace the representation with the compressed JSON), but I would consider them ugly. – user202729 Oct 01 '20 at 15:16
  • 1
    You are right about in your comment on my now deleted answer, inner quotes make this way more harder than it seems, so much that it may be a good idea to change your `d` string to something that does have such inner quotes. – Kaiido Oct 06 '20 at 01:30
  • That's an interesting question. I'd either implement a custom minifier or a custom prettyfier. JSON has a rather simple structure and therefore should be easy to parse. – htho Oct 06 '20 at 18:12
  • @Kaiido: You are right, I added inner quotes to the example. – Konrad Höffner Oct 07 '20 at 07:35

1 Answers1

2

Every time you call JSON.stringify on an object (or part of an object) more than once, you're going to end up with escaped speech marks. In this case, after the inner JSON.stringify function has been applied inside the replacer function, there is no way to tell the outer JSON.stringify function not to treat that instance as it would any other string.

So, I think if your goal is to 'partially' stringify an object, you need to write a function that implements that. For this problem, I would suggest something like this:

data = {
  "small": {
    "a": 1,
    "b": [2, 3, 4, 5],
    "c": [2, {"test":"example"}, [4,4], 5],
    "large": {
      "myarray": [1, 2, 3, 4],
      "b": 2,
      "c": 3,
      "d": "the \"large\" object should not be prettified"
    }
  },
  "large": {
    "a": 1,
    "b": 2,
    "c": 3,
    "d": "the \"large\" object should not be prettified"
  }
}

function print(obj, exclude, space) {
  let recur = (obj, spacing, inarray) => {
    let txt = '';

    if (inarray) {
      if (Array.isArray(obj)) {        
        txt += '[';

        for(let i=0;i<obj.length;i++) {
          txt += recur(obj[i], spacing + space, true);
        };

        txt = txt.substr(0, Math.max(1,txt.length - 2)) + ']';
        
      } else if (typeof obj === 'object' && obj !== null) {
        txt += '{' + recur(obj, spacing + space, false) + '\n' + spacing + '}';
      } else if (typeof obj === 'string') {
        txt += obj.replaceAll(/\"/g, '\\"') + '"';
      } else {
        txt += obj;
      };
      
      return txt + ', ';
      
    } else {
      for (let key of Object.keys(obj)) {
        if (exclude.indexOf(key) !== -1) {
          txt += '\n' + spacing + '"' + key + '": ' + JSON.stringify(obj[key]);
        } else if (Array.isArray(obj[key])) {
          txt += '\n' + spacing + '"' + key + '": [';
          
          for(let i=0;i<obj[key].length;i++) {
            txt += recur(obj[key][i], spacing + space, true);
          };
          
          txt = txt.substr(0, Math.max(1,txt.length - 2)) + ']';
          
        } else if (typeof obj[key] === 'object' && obj[key] !== null) {
          txt += '\n' + spacing + '"' + key + '": {' + recur(obj[key], spacing + space, false) + '\n' + spacing + '}';
        } else if (typeof obj[key] === 'string') {
          txt += '\n' + spacing + '"' + key + '": "' + obj[key].replaceAll(/\"/g, '\\"') + '"';
        } else {
          txt += '\n' + spacing + '"' + key + '": ' + obj[key];
        };
        
        txt += ',';
      };
      
      return txt.substr(0, txt.length - 1);
    };

  };
  return (Array.isArray(obj) ? '[' + recur(obj, space, true) + '\n' + ']' : '{' + recur(obj, space, false) + '\n' + '}');
};

document.getElementById("json").innerText = print(data, ['large'], '\t');
<html>
<body>
  <pre id="json">
</pre>
</body>
</html>

Here, a recursive function is used, which loops through the keys of an object and appends the key-value pair to a string, handling for different data types. If the key is found in the exclude array, then it is stringified.

By looping through the keys and conditionally applying JSON.stringify where required, you are effectively avoiding the above issue.

I'm sure there may be edge cases to watch out for when writing the JSON yourself instead of using a built-in function, but you could adapt something like this to suit your specific needs.

EDIT: answer updated to better consider arrays within the object.

Konrad Höffner
  • 11,100
  • 16
  • 60
  • 118
sbgib
  • 5,580
  • 3
  • 19
  • 26