43

I have a JavaScript ES6 class that has a property set with set and accessed with get functions. It is also a constructor parameter so the class can be instantiated with said property.

class MyClass {
  constructor(property) {
    this.property = property
  }

  set property(prop) {
  // Some validation etc.
  this._property = prop
  }

  get property() {
    return this._property
  }
}

I use _property to escape the JS gotcha of using get/set that results in an infinite loop if I set directly to property.

Now I need to stringify an instance of MyClass to send it with a HTTP request. The stringified JSON is an object like:

{
   //...
   _property:
}

I need the resulting JSON string to preserve property so the service I am sending it to can parse it correctly. I also need property to remain in the constructor because I need to construct instances of MyClass from JSON sent by the service (which is sending objects with property not _property).

How do I get around this? Should I just intercept the MyClass instance before sending it to the HTTP request and mutate _property to property using regex? This seems ugly, but I will be able to keep my current code.

Alternatively I can intercept the JSON being sent to the client from the service and instantiate MyClass with a totally different property name. However this means a different representation of the class either side of the service.

Thomas Chia
  • 455
  • 1
  • 4
  • 9

7 Answers7

57

You can use toJSON method to customise the way your class serialises to JSON:

class MyClass {
  constructor(property) {
    this.property = property
  }

  set property(prop) {
  // Some validation etc.
  this._property = prop
  }

  get property() {
    return this._property
  }

  toJSON() {
    return {
      property: this.property
    }
  }
}
Amadan
  • 191,408
  • 23
  • 240
  • 301
24

If you want to avoid calling toJson, there is another solution using enumerable and writable:

class MyClass {

  constructor(property) {

    Object.defineProperties(this, {
        _property: {writable: true, enumerable: false},
        property: {
            get: function () { return this._property; },
            set: function (property) { this._property = property; },
            enumerable: true
        }
    });

    this.property = property;
  }

}
Richard Časár
  • 323
  • 2
  • 8
  • You don't call `toJSON`. It is automatically called by `JSON.stringify`. See [`JSON.stringify`](http://www.ecma-international.org/ecma-262/6.0/#sec-json.stringify) step 12, and [`SerializeJSONProperty`](http://www.ecma-international.org/ecma-262/6.0/#sec-serializejsonproperty) step 3. – Amadan Mar 13 '17 at 23:55
  • 5
    That is a good point. Anyway, when you have more properties in your class toJSON method gets bigger and more complex. You need to update it everytime you add/remove a property. I still think it is better to avoid it. – Richard Časár Mar 18 '17 at 15:10
  • FYI this also works with constructor function style "classes". – Micah Epps Feb 16 '18 at 18:01
13

As mentioned by @Amadan you can write your own toJSON method.

Further more, in order to avoid re-updating your method every time you add a property to your class you can use a more generic toJSON implementation.

class MyClass {

  get prop1() {
    return 'hello';
  }
  
  get prop2() {
    return 'world';
  }

  toJSON() {

    // start with an empty object (see other alternatives below) 
    const jsonObj = {};

    // add all properties
    const proto = Object.getPrototypeOf(this);
    for (const key of Object.getOwnPropertyNames(proto)) {      
      const desc = Object.getOwnPropertyDescriptor(proto, key);
      const hasGetter = desc && typeof desc.get === 'function';
      if (hasGetter) {
        jsonObj[key] = desc.get();
      }
    }

    return jsonObj;
  }
}

const instance = new MyClass();
const json = JSON.stringify(instance);
console.log(json); // outputs: {"prop1":"hello","prop2":"world"}

If you want to emit all properties and all fields you can replace const jsonObj = {}; with

const jsonObj = Object.assign({}, this);

Alternatively, if you want to emit all properties and some specific fields you can replace it with

const jsonObj = {
    myField: myOtherField
};
Alon Bar
  • 482
  • 9
  • 10
  • That seems to produce both `_property` and `property` – Bergi Feb 24 '18 at 11:27
  • You are right. I wasn't paying enough attention to the details of the quedtion :( Never the less it is easy to fix. Just omit the Object.assign call and replace it with an empty object. This way you get all the properties without the fields. – Alon Bar Feb 25 '18 at 06:42
  • I've edited the answer to more accurately fit the required result (and other common use cases). – Alon Bar Feb 25 '18 at 07:00
  • 3
    I had to make 2 changes to make this code work: replace `const key of Object.keys(proto)` (only returns enumerable properties) with `const key of Object.getOwnPropertyNames(proto)` (returns all properties) and `jsonObj[key] = desc.get();` (returns "undefined" instead of the value of the property) with `jsonObj[key] = this[key];` – bits Jun 10 '18 at 15:43
  • I am actually using it in TypeScript and it works either way but you're right and I've updated the answer to use `Object.getOwnPropertyNames`, thanks! As for the second part, as you can see from the now runnable snippet, it seems to work. – Alon Bar Jun 10 '18 at 20:24
13

I made some adjustments to the script of Alon Bar. Below is a version of the script that works perfectly for me.

toJSON() {
        const jsonObj = Object.assign({}, this);
        const proto = Object.getPrototypeOf(this);
        for (const key of Object.getOwnPropertyNames(proto)) {
            const desc = Object.getOwnPropertyDescriptor(proto, key);
            const hasGetter = desc && typeof desc.get === 'function';
            if (hasGetter) {
                jsonObj[key] = this[key];
            }
        }
        return jsonObj;
    }
bits
  • 672
  • 2
  • 7
  • 26
1

Use private fields for internal use.

class PrivateClassFieldTest {
    #property;
    constructor(value) {
        this.property = value;
    }
    get property() {
        return this.#property;
    }
    set property(value) {
        this.#property = value;
    }
}

class Test {
 constructor(value) {
  this.property = value;
 }
 get property() {
  return this._property;
 }
 set property(value) {
  this._property = value;
 }
}

class PublicClassFieldTest {
 _property;
 constructor(value) {
  this.property = value;
 }
 get property() {
  return this.property;
 }
 set property(value) {
  this._property = value;
 }
}

class PrivateClassFieldTest {
 #property;
 constructor(value) {
  this.property = value;
 }
 get property() {
  return this.#property;
 }
 set property(value) {
  this.#property = value;
 }
}

console.log(JSON.stringify(new Test("test")));
console.log(JSON.stringify(new PublicClassFieldTest("test")));
console.log(JSON.stringify(new PrivateClassFieldTest("test")));
0

I've made an npm module named esserializer to solve such problem: stringify an instance of JavaScript class, so that it can be sent with HTTP request:

// Client side
const ESSerializer = require('esserializer');
const serializedText = ESSerializer.serialize(anInstanceOfMyClass);
// Send HTTP request, with serializedText as data

On service side, use esserializer again to deserialize the data into a perfect copy of anInstanceOfMyClass, with all getter/setter fields (such as property) retained:

// Node.js service side
const deserializedObj = ESSerializer.deserialize(serializedText, [MyClass]);
// deserializedObj is a perfect copy of anInstanceOfMyClass
shaochuancs
  • 15,342
  • 3
  • 54
  • 62
0

I ran into the same issue but I have no access to the class construction and I'm not able to add or override the ToJson method

here is the solution that helped me solve it

a simple class with getters and properties

class MyClass {
    jack = "yoo"
    get prop1() {
        return 'hello';
    }

    get prop2() {
        return 'world';
    }

}

a class with a child class and also child object with getters

class MyClassB {
    constructor() {
        this.otherClass = new MyClass()
    }
    joe = "yoo"
    otherObject = {
        youplaboum: "yoo",
        get propOtherObject() {
            return 'propOtherObjectValue';
        }
    }
    get prop1() {
        return 'helloClassB';
    }


    get prop2() {
        return 'worldClassB';
    }

}

here is the magic recursive function inspired by the ToJSON made by @bits

const objectWithGetters = function (instance) {
    const jsonObj = Object.assign({}, instance);
    const proto = Object.getPrototypeOf(instance);
    for (const key of Object.getOwnPropertyNames(proto)) {
        const desc = Object.getOwnPropertyDescriptor(proto, key);
        const hasGetter = desc && typeof desc.get === 'function';
        if (hasGetter) {
            jsonObj[key] = desc.get();
        }
    }
    for (let i in jsonObj) {
        let value = jsonObj[i];
        if (typeof value === "object" && value.constructor) {
            jsonObj[i] = objectWithGetters(value);
        }
    }
    return jsonObj;
}


const instance = new MyClassB();
const jsonObj = objectWithGetters(instance)
console.log(jsonObj)

let json = JSON.parse(jsonObj);
console.log(json)
benraay
  • 783
  • 8
  • 14