3

I am looking for a way to write a JSON object to a file, but maintaining the same formatting with the orignal. I have managed to write the content using writeFileSync(path,data) and JSON.stringify() but struggling to figure out how to customise the formatting. The options JSON.stringify accepts seem to format only the number of spaces.

Is there any way tho using JSON.stringify to generate the following formatting

{
  "key1"           :{"key": "value1","key": "value2"},
  "key2"           :{"key": "value1","key": "value2"}
}

instead of the one generated by default

{
  "key1": 
   {
     "key": "value1",
     "key": "value2"
   },
  "key2": 
   {
     "key": "value1",
     "key": "value2"
   },
}
Alex
  • 317
  • 4
  • 16

3 Answers3

3

Unfortunately not, you can use the space argument to define the spacing of the objects as they fall in on the tree. What you're proposing would require custom formatting using something like regular expressions to format the strings after you stringify it.

Below you'll find some sample code to do what you're looking to do however. You can append all of these into a single command like JSON.stringify(myJSON, null, ' ').replace(/: \{\n\s+/g, ': {').replace(/",\n\s+/g, ', ').replace(/"\n\s+\}/g, '}');, however so you could see what I did, I broke it down step by step.

const myJSON = {
  "key1":{"key": "value1","key2": "value2"},
  "key2":{"key": "value1","key2": "value2"}
}

let myString = JSON.stringify(myJSON, null, ' ').replace(/: {/g, `${' '.repeat(5)}: \{`); //Set the key spacing
myString = myString.replace(/: \{\n\s+/g, ': {'); //Bring the child bracket on the same line
myString = myString.replace(/",\n\s+/g, ', '); //Bring all the other objects for that array up
myString = myString.replace(/"\n\s+\}/g, '}'); //Pull the closing bracket on the same line

const myCompactString = JSON.stringify(myJSON, null, ' ').replace(/: {/g, `${' '.repeat(5)}: \{`).replace(/: \{\n\s+/g, ': {').replace(/",\n\s+/g, ', ').replace(/"\n\s+\}/g, '}'); //Done all at once

console.log(`myString: ${myString}`);
console.log(`myCompactString: ${myCompactString}`);
Trasiva
  • 494
  • 14
  • 29
0

Building on KevynTD's great answer!

to keep Arrays of strings ['song', 'bird'] from changing to {'0': 'song', '1': 'bird'} index keyed objects. Add the if (Array.isArray(this)) obj = this; to the singleLineOBJ function.

    const singleLineOBJ = function () {
        let obj = { ...this }; // To not change any toJSON
        if (Array.isArray(this))
            obj = this; // override converting array into object

        delete obj.toJSON // To not fall in a stringify loop
mj1701
  • 41
  • 1
-1

I want format like this:

var house = {
    family: {
        surname: "Smith",
        people: 4,
        pets: {
            dogs: {number:1, names:["toby"]},
            cats: {number:2, names:["bob", "boo"]},
            platypus: {number:1, names:["perry"], codename: ["agent p"]},
        }
    },
    livingRoom: [
        {name:"couch", amount:2},
        {name:"shelf", amount:1},
        {name:"nightstand", amount:1},
        {name:"television", amount:1},
    ],
    bedroom: [
        {name:"bed", amount:1},
        {name:"wardrobe", amount:1},
        {name:"shelf", amount:2},
    ],
}

It is a similar problem, but with more specific object keys, within a more complex tree of objects, and it started to get quite complicated to do this with just a regex. So, I created a function using toJSON and the stringify replacer as tools to handle this.

The function I created has some limitations, like just being able to define singleLine and multiLine only objects, and not numeric or text lists for example. It also has the limitation of some specific characters that are used for a replacement scheme, they are unusual characters, but look to confirm if you must replace them. The characters are four: '╲' (fake \), '”' (fake "), '→' and '←', if your object contains any of them you can replace them at the very beginning of the function.

Here an example with the function working:

// Custom Stringfy
const customStringify = function (
  obj,
  replacer,
  space = "\t",
  {
    singlelineObjectKeys = [],
    multilineObjectKeys = [],
    singlelineChildKeys = [],
    multilineChildKeys = [],
    singlelineInsideList = [],
  }
) {
  // WARNING
  // - This function will make a mess if your Object contain some of the following characters:
  const fakeNewLine = `╗`; // (replace \n in middle of process)
  const fakeTab = `╦`; // (replace \t in middle of process)
  const fakeQuote = `║`; // (replace " in middle of process)
  const startString = `╠`; // (add to start in middle of process)
  const endString = `╣`; // (add to end in middle of process)
  // So a solution in this case can be replace this chars by others not used (dont use characters that can mess the regex)

  // First make a stringify to solve any toJSON in the main object, then copy the main object stringfied to create all the necessary new toJSON
  let objModified = JSON.parse(JSON.stringify(obj, replacer));

  // Convert an entire object to single line string
  const singleLineOBJ = function () {
    // To not change any toJSON
    const obj = Array.isArray(this) ? [...this] : { ...this };
    // To not fall in a stringify loop
    delete obj.toJSON;

    // Mark the startString and endString
    return (
      startString +
      JSON.stringify(obj)
        // Replace all " by fakeQuote
        .replace(/"/g, fakeQuote) +
      endString
    );
  };

  // Convert an entire object to multi line string
  const multiLineOBJ = function () {
    // To not change any toJSON
    const obj = Array.isArray(this) ? [...this] : { ...this };
    // To not fall in a stringify loop
    delete obj.toJSON;

    // Mark the startString and endString
    return (
      startString +
      JSON.stringify(obj, null, "\t")
        // Replace all " by fakeQuote
        .replace(/"/g, fakeQuote)
        // Replace \n using fakeNewLine
        .replace(/\n/g, fakeNewLine)
        // Replace \t using fakeTab
        .replace(/\t/g, fakeTab) +
      endString
    );
  };

  // Checks all keys on the object
  const throughEveryKey = function (key, value) {
    let obj = this;
    // objects inside specific keys to become single-line
    if (singlelineObjectKeys.includes(key)) {
      obj[key].toJSON = singleLineOBJ;
    }
    // objects inside specific keys to become multi-line
    if (multilineObjectKeys.includes(key)) {
      obj[key].toJSON = multiLineOBJ;
    }
    // objects containing the following keys to become single-line
    if (singlelineChildKeys.includes(key)) {
      obj.toJSON = singleLineOBJ;
    }
    // objects containing the following keys to become multi-line
    if (multilineChildKeys.includes(key)) {
      obj.toJSON = multiLineOBJ;
    }
    // names of list of objects to each list-item become single-line
    if (singlelineInsideList.includes(key)) {
      obj[key].forEach(
        (objectInsideList) => (objectInsideList.toJSON = singleLineOBJ)
      );
    }
    return value;
  };

  // Just use stringify to go through all object keys, and apply the function to implement "toJSON" in right places, the result of stringify is not used in this case (WIP)
  JSON.stringify(objModified, throughEveryKey);

  // Use stringfy with right replacers, end result
  return (
    JSON.stringify(objModified, null, "\t")
      // Put in all start of line the right number of Tabs
      .replace(new RegExp("(?:(?<=(?<leadTab>^\t*).+?)(?<newLine>" + fakeNewLine + ")(?=.+?))+", "gm"), "$&$1")
      // Replace the fake tab by the real one
      .replace(new RegExp(fakeTab, "gm"), "\t")
      // Replace the fake new line by the real one
      .replace(new RegExp(fakeNewLine, "gm"), "\n")
      // Replace the fake quote by the real one
      .replace(new RegExp(fakeQuote, "gm"), '"')
      // Remove start and end of line from the stringfied object
      .replace(new RegExp('"' + startString, "gm"), "")
      .replace(new RegExp(endString + '"', "gm"), "")
      // Replace tab by custom space
      .replace(/(?<=^\t*)\t/gm, space)
  );
};

var house = {
  family: {
    surname: "Smith",
    people: 4,
    pets: {
      dogs: {
        number: 1,
        names: ["toby"],
      },
      cats: {
        number: 2,
        names: ["bob", "boo"],
      },
      platypus: {
        number: 1,
        names: ["perry"],
        codename: ["agent p"],
      },
    },
  },
  livingRoom: [
    {
      name: "couch",
      amount: 2,
    },
    {
      name: "shelf",
      amount: 1,
    },
    {
      name: "nightstand",
      amount: 1,
    },
    {
      name: "television",
      amount: 1,
    },
  ],
  bedroom: [
    {
      name: "bed",
      amount: 1,
    },
    {
      name: "wardrobe",
      amount: 1,
    },
    {
      name: "shelf",
      amount: 2,
    },
  ],
};

console.log("A custom stringify:\n\n");
console.log(
  customStringify(house, null, "  ", {
    singlelineObjectKeys: ["dogs", "cats", "platypus"],
    multilineObjectKeys: ["family"],
    multilineChildKeys: [],
    singlelineChildKeys: [],
    singlelineInsideList: ["livingRoom", "bedroom"],
  })
);

console.log("\n\n\n");

console.log("A normal stringify:\n\n");
console.log(JSON.stringify(house, null, "  "));

You need to pass some information to work, you don't need to use everything, but I'll explain the cases that I left prepared to format between multiLine and singleLine:

singlelineObjectKeys: Put here the keys for objects / arrays that need to be on a single line

multilineObjectKeys: Place here the keys of objects / arrays that need to be formatted in several lines

multilineChildKeys: If you do not want to specify a parent object, as they can have many, specify a child key for the parent object to be formatted on multiple lines

singlelineChildKeys: If you don't want to specify a parent object, as they can have many, specify a child key for the parent object to be formatted on a single line

singlelineInsideList: If you have a list of objects, and you want all objects within that list to be formatted in a single list, put the key for that list here

In the example above the code used was this:

customStringify(house, null, '\t', {
    singlelineObjectKeys: ["dogs","cats","platypus"],
    multilineObjectKeys: ["family"],
    singlelineInsideList: ["livingRoom","bedroom"],
})

But this will result the same for example:

customStringify(house, null, '\t', {
    singlelineChildKeys: ["name","names"],
})

As I had researched this in several places and found nothing about it, I register my solution here (which was one of the places I looked for). Feel free to use!

Edit:

  • Updated to receive lists as single line or multiline on objects, thanks to @mj1701's suggestion.
  • Updated way to replace newline and tab, made code 20% faster
  • Old unresolved problem: After defining singlelineInsideList, if you put a multilineObjectKeys that is inside, the code tab has a problem, in progress

I am modifying the code in free time, it is not 100% finished, but it already works in several tested cases.

If you found any bugs, please comment here!

KevynTD
  • 487
  • 4
  • 9