48

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.

3
  • 1
    MyClass.prototype.toJson might help you Commented Feb 8, 2017 at 7:57
  • 2
    @StephenBugsKamenar: toJSON - note the capitalisation Commented Feb 8, 2017 at 8:00
  • Related: Convert ES6 Class with Symbols to JSON Commented Feb 8, 2017 at 8:14

7 Answers 7

60

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 } } } 
Sign up to request clarification or add additional context in comments.

2 Comments

I'd recommend calling the getter though, not ._property
Thanks, should have thought of this!
25

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; } } 

3 Comments

You don't call toJSON. It is automatically called by JSON.stringify. See JSON.stringify step 12, and SerializeJSONProperty step 3.
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.
FYI this also works with constructor function style "classes".
14

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; } 

1 Comment

This one works great and can easily be put into a base class. Great!
14

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 }; 

5 Comments

That seems to produce both _property and property
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.
I've edited the answer to more accurately fit the required result (and other common use cases).
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];
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.
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")));

Comments

1

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 

Comments

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) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.