24

I am using getter/setter accessors in TypeScript. As it is not possible to have the same name for a variable and method, I started to prefix the variable with a lower dash, as is done in many examples:

private _major: number;

get major(): number {
  return this._major;
}
set major(major: number) {
  this._major = major;
}

Now when I use the JSON.stringify() method to convert the object into a JSON string, it will use the variable name as the key: _major.

As I don't want the JSON file to have all keys prefixed with a lower dash, is there any possibility to make TypeScript use the name of the getter method, if available? Or are there any other ways to use the getter/setter methods but still produce a clean JSON output?

I know that there are ways to manually modify the JSON keys before they are written to the string output. I am curious if there is simpler solution though.

Here is a JSFiddle which demonstrates the current behaviour.

Dominic
  • 4,572
  • 3
  • 25
  • 36
  • 4
    I Suppose another approach would be to use capitalisation of the properties, e.g. get Major()... like the C# naming convention for properties – Forge_7 Jan 10 '17 at 17:19

6 Answers6

12

No, you can't have JSON.stringify using the getter/setter name instead of the property name.

But you can do something like this:

class Version {
    private _major: number;

    get major(): number {
        return this._major;
    }

    set major(major: number) {
        this._major = major;
    }

    toJsonString(): string {
        let json = JSON.stringify(this);
        Object.keys(this).filter(key => key[0] === "_").forEach(key => {
            json = json.replace(key, key.substring(1));
        });

        return json;
    }
}

let version = new Version();
version.major = 2;
console.log(version.toJsonString()); // {"major":2}
Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • 8
    With reference to the following answer, you could just have defined a toJSON function on your object which automatically gets used by JSON.stringify(yourclassobject) https://stackoverflow.com/questions/29705211/object-defineproperty-on-a-prototype-prevents-json-stringify-from-serializing-it – jarodsmk Jun 07 '17 at 06:39
  • @N15M0_jk yeah, that's true, but I prefer not to "override" the behavior of `JSON.stringify` and instead just use `version.toJsonString()` – Nitzan Tomer Jun 07 '17 at 08:17
  • This produce error: TypeError: myObject.toJsonString is not a function – IntoTheDeep Jan 16 '18 at 12:15
  • @TeodorKolev I just checked this exact code in the typescript playground and it worked great. – Nitzan Tomer Jan 16 '18 at 13:43
  • 1
    Use `toJSON`. For some reason, nobody uses `toJSON` or `valueOf` when they absolutely should. I guess not a lot of people know these exist. For instance, `class User {...}` could leverage the method `valueOf() { return this.id; }` so it can be used as `var id = +user;`. Not sure if that provides you with anymore leverage. – Cody Jan 25 '19 at 18:44
11

based on @Jan-Aagaard solution I have tested this one

public toJSON(): string {
    let obj = Object.assign(this);
    let keys = Object.keys(this.constructor.prototype);
    obj.toJSON = undefined;
    return JSON.stringify(obj, keys);
}

in order to use the toJSON method

alacambra
  • 549
  • 5
  • 12
  • 1
    To get full object I used let keys = Object.keys(this).concat(Object.keys(this.constructor.prototype)) instead – Israel Lot Mar 16 '18 at 01:16
  • This is the right answer. But I don't think you need the line `obj.toJSON = undefined;` as `JSON.stringify` ignores methods altogether. Please let me know if I'm incorrect. – Cody Jan 25 '19 at 18:46
  • 1
    @Cody Yes, it ignores methods on serializing, but if the `toJSON` method remains defined it will call it instead of serializing the object directly and this generates a recursive eternal loop. You need to "undefine" the `toJSON` to avoid that. – Dinei Apr 29 '19 at 19:57
5

I think iterating through the properties and string manipulating is dangerous. I would do using the prototype of the object itself, something like this:

public static toJSONString() : string {
    return JSON.stringify(this, Object.keys(this.constructor.prototype)); // this is version class
}
Jan Aagaard
  • 10,940
  • 8
  • 45
  • 80
JoSeTe4ever
  • 473
  • 6
  • 17
  • 2
    this is working, but it's only returning the setters and getters. how can I get all the other properties that are not getters and setters? – hsantos Apr 21 '17 at 09:32
3

I've written a small library ts-typed, which generate getter/setter for runtime typing purpose. I've faced the same problem when using JSON.stringify(). So i've solved it by adding a kind of serializer, and proposing to implement a kind of toString (in Java) buy calling it toJSON.

Here is an example:

import { TypedSerializer } from 'ts-typed';

export class RuntimeTypedClass {
    private _major: number;

    get major(): number {
       return this._major;
    }

    set major(major: number) {
       this._major = major;
    }
    /**
    * toString equivalent, allows you to remove the _ prefix from props.
    *
    */
    toJSON(): RuntimeTypedClass {
        return TypedSerializer.serialize(this);
    }
}
Yacine MEDDAH
  • 1,211
  • 13
  • 17
2

A new answer to an old question. For situations where there is no private field for a getter/setter, or where the private field name is different to the getter/setter, we can use the Object.getOwnPropertyDescriptors to find the get methods from the prototype.

https://stackoverflow.com/a/60400835/2325676

We add the toJSON function here so that it works with JSON.stringify as mentioned by other posters. This means we can't call JSON.stringify() within toJSON as it will cause an infinite loop so we clone using Object.assign(...)

I also removed the _private fields as a tidyup measure. You may want to remove other fields you don't want to incude in the JSON.

public toJSON(): any {

    //Shallow clone
    let clone: any = Object.assign({}, this); 

    //Find the getter method descriptors
    //Get methods are on the prototype, not the instance
    const descriptors = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this))

    //Check to see if each descriptior is a get method
    Object.keys(descriptors).forEach(key => {
        if (descriptors[key] && descriptors[key].get) {

            //Copy the result of each getter method onto the clone as a field
            delete clone[key];
            clone[key] = this[key]; //Call the getter
        }
    });

    //Remove any left over private fields starting with '_'
    Object.keys(clone).forEach(key => {
        if (key.indexOf('_') == 0) {
            delete clone[key];
        }
    });

    //toJSON requires that we return an object
    return clone;
}
Daniel Flippance
  • 7,734
  • 5
  • 42
  • 55
  • Using TypeScript 5.0.4 I have a compiler error at `clone[key] = this[key];`. The error is `error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'AccountRecord'.` Don't know how to resolve this. `this` can't be casted so something else, though. In my this is an instance of a class named `AccountRecord` which doesn't have an indexer that takes a string. – Manfred Jun 02 '23 at 05:31
2

Isn't dynamic but it work

export class MyClass{
     text: string

     get html() {
         return this.text.toString().split("\n").map(e => `<p>${e}</p>`).join('');
     }

     toJson(): string {
         return JSON.stringify({ ...this, html: this.html })
     }
}

In calling

console.log(myClassObject.toJson())