7

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:

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?
Community
  • 1
  • 1
Alberto De Caro
  • 5,147
  • 9
  • 47
  • 73

2 Answers2

6

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;
        }
    })()
}
Andreas Louv
  • 46,145
  • 13
  • 104
  • 123
  • 1
    Found that out at roughly the same time as you (after crashing Firefox during the course ;)). One comment: If you do it like this you break normal `JSON.stringify(obj)`, since it will interpret the object as a string and escape and add surrounding " chars. – filmor Oct 09 '12 at 09:03
  • @filmor I ran into the same problem. But think I found a solution using `fn.caller` – Andreas Louv Oct 09 '12 at 10:20
  • Interesting, but quite a lot of stuff just to use `toJSON` intead of `display` :). Note that there is a `non standard` remark in the `Function.caller` page. Anyway, an elegant solution. – Alberto De Caro Oct 09 '12 at 13:17
3

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;
}
lrsjng
  • 2,615
  • 1
  • 19
  • 23