16

I have been working with a fair bit of JSON parsing and passing in Javascript within Node.js and browsers recently and bumped into this conundrum.

Any objects I created using a constructor, cannot be fully serialized fully via JSON.stringify, UNLESS I initialised all values within the constructor individually! This means my prototype becomes essentially useless in designing these classes.

Can someone shed some light on why the following doesn't serialize as I expect?

var ClassA = function () { this.initialisedValue = "You can see me!" };
ClassA.prototype = { initialisedValue : "You can't see me!", uninitialisedValue : "You can't see me!" };
var a = new ClassA();
var a_string = JSON.stringify(a);

What happens:

a_string == { "initialisedValue" : "You can see me!" }

I would expect:

a_string == { "initialisedValue" : "You can see me!", "uninitialisedValue" : "You can't see me!" }


Update (01-10-2019):

Finally noticed @ncardeli 's Answer, which does allow us to do something like the following to achieve my above requirement (in 2019!):

Replace var a_string = JSON.stringify(a);

with var a_string = JSON.stringify(a, Object.keys(ClassA.prototype));

Full code:

var ClassA = function () { this.initialisedValue = "You can see me!" };
ClassA.prototype = { initialisedValue : "You can't see me!", uninitialisedValue : "You can't see me!" };
var a = new ClassA();
var a_string = JSON.stringify(a, Object.keys(ClassA.prototype));

console.log(a_string)
killercowuk
  • 1,313
  • 1
  • 11
  • 23
  • possible duplicate of [How to stringify inherited objects to JSON?](http://stackoverflow.com/questions/8779249/how-to-stringify-inherited-objects-to-json) – lonesomeday Sep 11 '12 at 12:21
  • http://stackoverflow.com/questions/5790620/how-to-stringify-a-whole-javascript-object-including-proto-properties – speg Sep 11 '12 at 12:24
  • I understand Methods cannot be serialized via JSON, but thanks lonesomeday as you've pointed me to a discussion about this. Obviously it's a documented 'issue', not sure why my searches didn't bring this up. Thanks! – killercowuk Sep 11 '12 at 12:25
  • @lonesomeday, I'd personally prefer some reference to standard that says "no, inherited properties do not count" instead of answers "that's just how it is". Oh, and after I typed this comment, here's the answer. :) – Oleg V. Volkov Sep 11 '12 at 12:26

3 Answers3

17

Simply because this is the way JSON works. From the ES5 spec:

Let K be an internal List of Strings consisting of the names of all the own properties of value whose [[Enumerable]] attribute is true.

This makes sense, because there is no mechanism in the JSON specification for preserving information that would be required to parse a JSON string back into a JavaScript object if inherited properties were included. In your example, how would this parsed:

{ "initialisedValue" : "You can see me!", "uninitialisedValue" : "You can't see me!" }

There is no information to parse it into anything other than a flat object with 2 key-value pairs.

And if you think about it, JSON is not intended to map directly to JavaScript objects. Other languages must be able to parse JSON strings into simple structures of name-value pairs. If JSON strings contained all the information necessary to serialize complete JavaScript scope chains, other languages may be less capable of parsing that into something useful. In the words of Douglas Crockford on json.org:

These [hash tables and arrays] are universal data structures. Virtually all modern programming languages support them in one form or another. It makes sense that a data format that is interchangeable with programming languages also be based on these structures.

James Allardice
  • 164,175
  • 21
  • 332
  • 312
  • Thanks James, I guess this puts this issue to bed. Shame as makes prototypes essentially useless in my case! Will have to resort to bloaty constructors then! – killercowuk Sep 11 '12 at 12:37
  • @killercowuk - You could come up with your own stringified format for your objects. I've edited my answer to include some further rationale behind the reasons for this. – James Allardice Sep 11 '12 at 12:42
  • Cheers James, I do get the rational, however feel a little missold by the abbreviation JSON - JavaScript Object Notation; however fully appreciate it is fundamentally a "lightweight data-interchange format" (json.org) – killercowuk Sep 11 '12 at 12:48
7

I'd like to add that, even though JSON.stringify will only stringify the object's own properties, as explained in the accepted answer, you can alter the behavior of the stringification process by specifying an array of String as the second parameter of JSON.stringify (called a replacer array).

If you specify an array of String with the whitelist of properties to stringify, the stringification algorithm will change its behavior and it will consider properties in the prototype chain.

From ES5 spec:

  1. If PropertyList is not undefined, then

    a. Let K be PropertyList.

  2. Else

    a. Let K be an internal List of Strings consisting of the names of all the own properties of value whose [[Enumerable]] attribute is true. The ordering of the Strings should be the same as that used by the Object.keys standard built-in function.

If you know the name of the properties of the object to stringify beforehand, you can do something like this:

var a_string = JSON.stringify(a, ["initialisedValue", "uninitialisedValue"]);
//  a_string == { "initialisedValue" : "You can see me!", "uninitialisedValue" : "You can't see me!" }
ncardeli
  • 3,452
  • 2
  • 22
  • 27
  • 2
    Sorry I didn't see this sooner, thanks for this. This actually does present a way to achieve my original requirement (a few years later :) ). I'm going to edit my answer above with the solution. – killercowuk Oct 01 '19 at 10:08
0

There is another possibility to still have properties from the prototype to be stringified.

As of the JSON spec (15.12.3 stringify http://es5.github.io/#x15.12.3):

[...]
2. If Type(value) is Object, then
    a. Let toJSON be the result of calling the [[Get]] internal method of value with argument "toJSON".
    b. If IsCallable(toJSON) is true
        i. Let value be the result of calling the [[Call]] internal method of toJSON passing value as the this value and with an argument list consisting of key.
[...]

So yu can write your own JSON stringifier function. A general implementation could be:

class Cat {
    constructor(age) {
        this.age = age;
    }

    get callSound() {
        // This does not work as this getter-property is not enumerable
        return "Meow";
    }

    toJSON() {
        const jsonObj = {}
        const self = this; // If you can use arrow functions just use 'this'-keyword.

        // Object.keys will list all 'enumerable' properties
        // First we look at all own properties of 'this'
        Object.keys(this).forEach(function(k) {
            jsonObj[k] = self[k];
        });
        // Then we look at all own properties of this's 'prototype'
        Object.keys(Object.getPrototypeOf(this)).forEach(function(k) {
            jsonObj[k] = self[k];
        });

        return JSON.stringify(jsonObj);
    }
}

Object.defineProperty(Cat.prototype, 'callSound2', {
    // The 'enumerable: true' is important!
    value: "MeowMeow", enumerable: true
});


let aCat = new Cat(4);

console.log(JSON.stringify(aCat));
// prints "{\"age\":4,\"callSound2\":\"MeowMeow\"}"

This works as long as the right properties (those you actually want to be JSON stringified) are enumerable and those you dont want to be stringified aren't. So you need to be careful which properties you make enumerable or become implicitly enumerable when you assign values to 'this'.

Another possibility is to assign each property you actually want to be stringifed manually one by one. Which might be less error prone.

Lukas Willin
  • 345
  • 3
  • 9