Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

recursive JSON.stringify implementation

I am trying to learn recursion in Javascript, so I figured I'd rewrite the native JSON.stringify function using recursion as a challenge to myself. I almost got my code to work:

var my_stringify = function(obj){        
  value = obj[ Object.keys(obj)[0] ];
  index = Object.keys(obj)[0];

  delete obj[ Object.keys(obj)[0] ];

  // The value is just a simple string, not a nested object
  if (typeof value === 'string'){
    if (Object.keys(obj).length !== 0){
      // Continue recursion ..
      return '"' + index + '":"' + value + '",' + my_stringify(obj);
    }

    // This would be the base case with a string at the end. Stop recursion.
    return '"' + index + '":"' + value + '"}';
  }
  // The value is actually a nested object
  else{     
    if (Object.keys(obj).length !== 0){
    // Continue recursion ..
      return '"' + index + '":{' + my_stringify(value) + ',' + my_stringify(obj);
    }
    // This is the base case with a nested object at the end. Stringify it and end recursion.
    return '"' + index + '":{' + my_stringify(value) + '}';  
  }
}

Except for the fact that the first { in my answer is missing, and I can't figure out how to fix this bug.

E.g. my_stringify({foo: 'bar'}) returns "foo":"bar"} instead of {"foo":"bar"}.

Also, I'm aware I'm completely destroying the original object, is there any way to send over to recursion a reduced version of the original object without deleting anything (something like obj.slice(1))?

Any advice will be greatly appreciated !

like image 381
cilop Avatar asked Dec 02 '22 16:12

cilop


2 Answers

New answer to an old question

There's some painfully bad answers here that fail under even the simplest examples. This answer aims to answer the question exhaustively and demonstrate how an approach like this scales even when handling a wide variety of data types and ...

Corner cases

This function does a simple case analysis on a non-null data's constructor property and encodes accordingly. It manages to cover a lot of corner cases that you're unlikely to consider, such as

  • JSON.stringify(undefined) returns undefined
  • JSON.stringify(null) returns 'null'
  • JSON.stringify(true) returns 'true'
  • JSON.stringify([1,2,undefined,4]) returns '[1,2,null,4]'
  • JSON.stringify({a: undefined, b: 2}) returns '{ "b": 2 }'
  • JSON.stringify({[undefined]: 1}) returns '{ "undefined": 1 }'
  • JSON.stringify({a: /foo/}) returns { "a": {} }

So to verify that our stringifyJSON function actually works properly, I'm not going to test the output of it directly. Instead, I'm going to write a little test method that ensures the JSON.parse of our encoded JSON actually returns our original input value

// we really only care that JSON.parse can work with our result
// the output value should match the input value
// if it doesn't, we did something wrong in our stringifier
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test([1,2,3])     // should return [1,2,3]
test({a:[1,2,3]}) // should return {a:[1,2,3]}

Disclaimer: it should be obvious that the code I'm about to share is not meant to be used as an actual replacement for JSON.stringify – there's countless corner cases we probably didn't address. Instead, this code is shared to provide a demonstration for how we could go about such a task. Additional corner cases could easily be added to this function.


Runnable demo

Without further ado, here is stringifyJSON in a runnable demo that verifies excellent compatibility for several common cases

const stringifyJSON = data => {
  if (data === undefined)
    return undefined
  else if (data === null)
    return 'null'
  else if (data.constructor === String)
    return '"' + data.replace(/"/g, '\\"') + '"'
  else if (data.constructor === Number)
    return String(data)
  else if (data.constructor === Boolean)
    return data ? 'true' : 'false'
  else if (data.constructor === Array)
    return '[ ' + data.reduce((acc, v) => {
      if (v === undefined)
        return [...acc, 'null']
      else
        return [...acc, stringifyJSON(v)]
    }, []).join(', ') + ' ]'
  else if (data.constructor === Object)
    return '{ ' + Object.keys(data).reduce((acc, k) => {
      if (data[k] === undefined)
        return acc
      else
        return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])]
    }, []).join(', ') + ' }'
  else
    return '{}'
}

// round-trip test and log to console
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test(null)                               // null
test('he said "hello"')                  // 'he said "hello"'
test(5)                                  // 5
test([1,2,true,false])                   // [ 1, 2, true, false ]
test({a:1, b:2})                         // { a: 1, b: 2 }
test([{a:1},{b:2},{c:3}])                // [ { a: 1 }, { b: 2 }, { c: 3 } ]
test({a:[1,2,3], c:[4,5,6]})             // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] }
test({a:undefined, b:2})                 // { b: 2 }
test({[undefined]: 1})                   // { undefined: 1 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]
like image 89
Mulan Avatar answered Dec 09 '22 15:12

Mulan


You need to view recursion as going deeper into the object without actually altering the object. It looks like you're trying to use recursion to go sideways inside of an object.

I've written a version of stringify that handles basic object (no arrays or functions).

Here is the fiddle

Here is the code:

var my_stringify2 = function (obj) {
    var objKeys = Object.keys(obj);
    var keyValueArray = new Array();
    for (var i = 0; i < objKeys.length; i++) {
        var keyValueString = '"' + objKeys[i] + '":';
        var objValue = obj[objKeys[i]];
        keyValueString = (typeof objValue == "string") ? 
            keyValueString = keyValueString + '"' + objValue + '"' : 
            keyValueString = keyValueString + my_stringify2(objValue);
        keyValueArray.push(keyValueString);
    }
    return "{" + keyValueArray.join(",") + "}";
}

You want the recursion to do most of the work for you, and you should only need to handle basic conditions (which you already had). In my function the two acceptable conditions are string and object.

A string is handled on the spot, and an object is passed into the function recursively.

That's the key. You were passing the same object into the function repeatedly, removing the handled elements until you get to a point where the object is completely gone.

What I did instead was pass the value of that particular property if it were an object. If it's a string, just add it to the string and move along.

Take a look at the code and let me know if you have any questions. Notice that the object that I'm passing in has a nested object.

my_stringify2({
    foo: 'bar',
    bar: 'foo',
    foobar: {
        foo: 'bar',
        bar: 'foo'
    }
});

and the result is proper json

{"foo":"bar","bar":"foo","foobar":{"foo":"bar","bar":"foo"}} 

If you're looking to completely avoid a for loop, you can do the following

jsfiddle

in this one you pass the object like normal, but recursively you pass a key array, removing an element from the key array for each property.

a bit more complicated, so I added comments

var my_stringify2 = function (obj, objKeys) {
    var str = "";
    // keys haven't been loaded, either first pass, or processing a value of type object
    if (objKeys == undefined) { 
        objKeys = Object.keys(obj);
        str = "{"
    } else {
        // if keys array exists and is empty, no more properties to evaluate, return the end bracket
        if (objKeys.length == 0) {
            return "}";
        // array exists and isn't empty, that means it's a property and not the first property, add a comma    
        } else {
            str = ",";
        }
    }
    // add the property name
    str += '"' + objKeys[0] + '":';
    // get the value
    var objValue = obj[objKeys[0]];
    // if the value type is string, add the string, if it's an object, call this function again, but leave the objKeys undefined
    str +=
        (typeof objValue == "string") ? 
        '"' + objValue + '"' : 
         my_stringify2(objValue);    
    // remove the first element fromt the keys array
    objKeys.splice(0,1);
    //call the function for the next property
    return str + my_stringify2(obj, objKeys);
}
like image 31
Smeegs Avatar answered Dec 09 '22 16:12

Smeegs