Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stackoverflow error on Javascript toJSON custom method

Scenario

After reading this answer I realized that I could create object starting from a JSON literal.

So I guessed that I could do the opposite just using this useful JSON method: JSON.stringify(myObject).

So I did as follow:

function MyObject(id, value, desc)
{
  this.id = id;
  this.value = value;
  this.desc = desc;
  this.toJSON = function()
  {
    return JSON.stringify(this);
  }

}

But when I run this stuff (demo) a Maximum call stack size exceeded error occurs.

After googling a bit, I found two references that explain this behaviour:

  • the JSON.stringify() method at MDN.
  • the JSON in Javascript article at JSON.org

If I get right, .toJSON overrides the .stringify. So if the first one calls the second one a loop is generated.

Questions

  1. (general) Why this design choice? toJSON is a kind of reserved of special keyword?
  2. (specific) I solved the stackoverflow bug changing the .toJSON name into .display. Not so elegant. Is there another solution?
like image 774
Alberto De Caro Avatar asked Oct 09 '12 08:10

Alberto De Caro


2 Answers

Think it's because toJSON is semi reserved: stringify will check the object and see if it's has a method called toJSON and then try to call it to string the result.


A workaround can be: (Not sure about the reliablity of this code)

var obj = {
    value: 1,
    name: "John",
    toJSON: function() {
        var ret,
            fn = this.toJSON;

        delete this.toJSON;

        ret = JSON.stringify(this);

        this.toJSON = fn;

        return ret;
    }
}

Usage:

obj.toJSON(); // "{\"value\":1,\"name\":\"John\"}"
obj.lastName = "Smith";
obj.toJSON(); // "{\"value\":1,\"name\":\"John\",\"lastName\":\"Smith\"}"

Maybe using a clousure is a little prettier: (And then I think I can say it's safe)

var obj = {
    value: 1,
    name: "John",
    toJSON: (function() {
        function fn() {
            var ret;
            delete this.toJSON;

            ret = JSON.stringify(this);

            this.toJSON = fn;

            return ret;
        }
        return fn;
    })()
}

So after reading @filmor's comment i thoght about another way to handle this. Not that pretty but it works.

Using Function.caller I can detect if fn is called using JSON.stringify

var obj = {
    value: 1,
    name: "John",
    toJSON: (function() {
        return function fn() {
            var ret;

            delete this.toJSON;

            ret = JSON.stringify(this);

            if ( fn.caller === JSON.stringify ) {
                ret = JSON.parse( ret );
            }

            this.toJSON = fn;

            return ret;
        }
    })()
}
like image 193
Andreas Louv Avatar answered Nov 19 '22 09:11

Andreas Louv


Question 1, is toJSON reserved?

I'm not sure if it reserved, but for example the native Date object uses toJSON to create a stringified date representation:

(new Date()).toJSON();           // -> "2012-10-20T01:58:21.427Z"
JSON.stringify({d: new Date()}); // -> {"d":"2012-10-20T01:58:21.427Z"}"

Question 2, an easy solution:

create your custom stringify function that ignores toJSON methods (you may add it to the already existing global JSON):

JSON.customStringify = function (obj) {

    var fn = obj.toJSON;
    obj.toJSON = undefined;
    var json = JSON.stringify(obj);
    obj.toJSON = fn;
    return json;
}

now it's very easy to use in all your objects:

function MyObject(id, value, desc)
{
  this.id = id;
  this.value = value;
  this.desc = desc;
  this.toJSON = function()
  {
    return JSON.customStringify(this);
  }
}

To make it even more easy additionally add:

JSON.customStringifyMethod = function () {

    return JSON.customStringify(this);
}

Now your objects might look like:

function MyObject(id, value, desc)
{
  this.id = id;
  this.value = value;
  this.desc = desc;
  this.toJSON = JSON.customStringifyMethod;
}
like image 3
lrsjng Avatar answered Nov 19 '22 10:11

lrsjng