Implement JSON stringify in JavaScript.

Implement a simple polyfill for JSON.stringify() in JavaScript.

Example

console.log(JSON.stringify([{ x: 5, y: 6 }]));
// expected output: "[{"x":5,"y":6}]"

JSON.stringify() converts almost each javascript value to a string format except for a few.

We will break down this problem into two sub-problems and tackle them separately.

First, determine the typeof value and accordingly convert it to the string.

  • For function, symbol, undefined return "null".
  • For number, if the value is finite return the value as it is else return "null".
  • For boolean return it as it is and for string return the value in double quotes.
  • The last thing left is object, there are multiple cases to handle for object.
  • If it is Date, convert it to ISO string.
  • If it is a constructor of String, Boolean, or Number, convert it to those values only.
  • If it is an array, convert each value of the array and return it.
  • If it is a nested object recursively call the same function to stringify further.
// helper method
  // handle all the value types
  // and stringify accordingly
  static value(val) {
    switch(typeof val) {
      case 'boolean':
      case 'number':
        // if the value is finitie number return the number as it is 
        // else return null
        return isFinite(val) ? `${val}` : `null`;
      case 'string':
        return `"${val}"`;
      // return null for anything else
      case 'function':
      case 'symbol':
      case 'undefined':
        return 'null';
      // for object, check again to determine the objects actual type
      case 'object':
        // if the value is date, convert date to string
        if (val instanceof Date) {
          return `"${val.toISOString()}"`;
        }
        // if value is a string generated as constructor, // new String(value)
        else if(val.constructor === String){
          return `"${val}"`;
        }
        // if value is a number or boolean generated as constructor, // new String(value), new Boolean(true)
        else if(val.constructor === Number || val.constructor === Boolean){
          return isFinite(val) ? `${val}` : `null`;
        }
        // if value is a array, return key values as string inside [] brackets
        else if(Array.isArray(val)) {
          return `[${val.map(value => this.value(value)).join(',')}]`;
        }
        
        // recursively stingify nested values
        return this.stringify(val);
    }
  }

Second, we will handle some base cases like, if it is a null value, return 'null', if it is an object, get the appropriate value from the above method. At the end wrap the value inside curly braces and return them.

// main method
  static stringify(obj) { 
    // if value is not an actual object, but it is undefined or an array
    // stringifiy it directly based on the type of value
    if (typeof obj !== 'object' || obj === undefined || obj instanceof Array) {
      return this.value(obj);
    } 
    // if value is null return null
    else if(obj === null) {
      return `null`;
    }
    
    // remove the cycle of object
    // if it exists
    this.removeCycle(obj);
    
    // traverse the object and stringify at each level
    let objString = Object.keys(obj).map((k) => {
        return (typeof obj[k] === 'function') ? null : 
        `"${k}": ${this.value(obj[k])}`;
    });
    
    // return the stringified output
    return `{${objString}}`;
  }

The final case to handle the circular object, for that we will be using the removeCycle(obj) method to remove the cycle from the objects.

// helper method to remove cycle
  static removeCycle = (obj) => {
    //set store
      const set = new WeakSet([obj]);

      //recursively detects and deletes the object references
      (function iterateObj(obj) {
          for (let key in obj) {
              // if the key is not present in prototype chain
              if (obj.hasOwnProperty(key)) {
                  if (typeof obj[key] === 'object'){
                      // if the set has object reference
                      // then delete it
                      if (set.has(obj[key])){ 
                        delete obj[key];
                      }
                      else {
                        //store the object reference
                          set.add(obj[key]);
                        //recursively iterate the next objects
                          iterateObj(obj[key]);
                      }
                  }
              }
          }
      })(obj);
  }

Complete code.

class JSON {
  
  // main method
  static stringify(obj) { 
    // if value is not an actual object, but it is undefined or an array
    // stringifiy it directly based on the type of value
    if (typeof obj !== 'object' || obj === undefined || obj instanceof Array) {
      return this.value(obj);
    } 
    // if value is null return null
    else if(obj === null) {
      return `null`;
    }
    
    // remove the cycle from the object
    // if it exists
    this.removeCycle(obj);
    
    // traverse the object and stringify at each level
    let objString = Object.keys(obj).map((k) => {
        return (typeof obj[k] === 'function') ? null : 
        `"${k}": ${this.value(obj[k])}`;
    });
    
    // return the stringified output
    return `{${objString}}`;
  }
  
  // helper method
  // handle all the value types
  // and stringify accordingly
  static value(val) {
    switch(typeof val) {
      case 'boolean':
      case 'number':
        // if the value is finite number return the number as it is 
        // else return null
        return isFinite(val) ? `${val}` : `null`;
      case 'string':
        return `"${val}"`;
      // return null for anything else
      case 'function':
      case 'symbol':
      case 'undefined':
        return 'null';
      // for object, check again to determine the object's actual type
      case 'object':
        // if the value is date, convert date to string
        if (val instanceof Date) {
          return `"${val.toISOString()}"`;
        }
        // if value is a string generated as constructor, // new String(value)
        else if(val.constructor === String){
          return `"${val}"`;
        }
        // if value is a number or boolean generated as constructor, // new String(value), new Boolean(true)
        else if(val.constructor === Number || val.constructor === Boolean){
          return isFinite(val) ? `${val}` : `null`;
        }
        // if value is a array, return key values as string inside [] brackets
        else if(Array.isArray(val)) {
          return `[${val.map(value => this.value(value)).join(',')}]`;
        }
        
        // recursively stingify nested values
        return this.stringify(val);
    }
  }
  
  // helper method to remove cycle
  static removeCycle = (obj) => {
    //set store
      const set = new WeakSet([obj]);

      //recursively detects and deletes the object references
      (function iterateObj(obj) {
          for (let key in obj) {
              // if the key is not present in prototype chain
              if (obj.hasOwnProperty(key)) {
                  if (typeof obj[key] === 'object'){
                      // if the set has object reference
                      // then delete it
                      if (set.has(obj[key])){ 
                        delete obj[key];
                      }
                      else {
                        //store the object reference
                          set.add(obj[key]);
                        //recursively iterate the next objects
                          iterateObj(obj[key]);
                      }
                  }
              }
          }
      })(obj);
  }
};
let obj1 = {
  a: 1,
  b: {
   c: 2,
    d: -3,
    e: {
      f: {
        g: -4,
      },
    },
    h: {
      i: 5,
      j: 6,
    },
  }
}


let obj2 = {
  a: 1,
  b: {
    c: 'Hello World',
    d: 2,
    e: {
      f: {
        g: -4,
      },
    },
    h: 'Good Night Moon',
  },
}

// cricular object
const List = function(val){
  this.next = null;
  this.val = val;
};

const item1 = new List(10);
const item2 = new List(20);
const item3 = new List(30);

item1.next = item2;
item2.next = item3;
item3.next = item1;

console.log(JSON.stringify(item1));
console.log(JSON.stringify(obj1));
console.log(JSON.stringify(obj2));

console.log(JSON.stringify([{ x: 5, y: 6 }]));
// expected output: "[{"x":5,"y":6}]"

console.log(JSON.stringify([new Number(3), new String('false'), new Boolean(false), new Number(Infinity)]));
// expected output: "[3,"false",false]"

console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')]}));
// expected output: "{"x":[10,null,null,null]}"

console.log(JSON.stringify({a: Infinity}));

Output:
"{'next': {'next': {'val': 30},'val': 20},'val': 10}"

"{'a': 1,'b': {'c': 2,'d': -3,'e': {'f': {'g': -4}},'h': {'i': 5,'j': 6}}}"

"{'a': 1,'b': {'c': 'Hello World','d': 2,'e': {'f': {'g': -4}},'h': 'Good Night Moon'}}"

"[{'x': 5,'y': 6}]"

"[3,'false',false,null]"

"{'x': [10,null,null,null]}"

"{'a': null}"