120

I've done quite some research, but I'm not totally satisfied with what I found. Just to be sure here's my question: What is actually the most robust and elegant automated solution for deserializing JSON to TypeScript runtime class instances?

Say I got this class:

class Foo {
  name: string;
  GetName(): string { return this.name };
}

And say I got this JSON string for deserialization:

{"name": "John Doe"}

What's the best and most maintainable solution for getting an instance of a Foo class with the name set to "John Doe" and the method GetName() to work? I'm asking very specifically because I know it's easy to deserialize to a pure data-object. I'm wondering if it's possible to get a class instance with working methods, without having to do any manual parsing or any manual data copying. If a fully automated solution isn't possible, what's the next best solution?

wonea
  • 4,783
  • 17
  • 86
  • 139
Klaus
  • 1,201
  • 2
  • 9
  • 5
  • Since the `name` field is public, what´s the `GetName` method for? – Matze Apr 20 '15 at 22:12
  • 12
    I guess you can consider it a doctored example ;) The point was to get an answer that yields a true instance of class that includes all methods as well, not just a deserialized class instance that contains only the data, and doesn't allow to invoke the declared methods. – Klaus Apr 21 '15 at 07:57

5 Answers5

97

This question is quite broad, so I'm going to give a couple of solutions.

Solution 1: Helper Method

Here's an example of using a Helper Method that you could change to fit your needs:

class SerializationHelper {
    static toInstance<T>(obj: T, json: string) : T {
        var jsonObj = JSON.parse(json);

        if (typeof obj["fromJSON"] === "function") {
            obj["fromJSON"](jsonObj);
        }
        else {
            for (var propName in jsonObj) {
                obj[propName] = jsonObj[propName]
            }
        }

        return obj;
    }
}

Then using it:

var json = '{"name": "John Doe"}',
    foo = SerializationHelper.toInstance(new Foo(), json);

foo.GetName() === "John Doe";

Advanced Deserialization

This could also allow for some custom deserialization by adding your own fromJSON method to the class (this works well with how JSON.stringify already uses the toJSON method, as will be shown):

interface IFooSerialized {
    nameSomethingElse: string;
}

class Foo {
  name: string;
  GetName(): string { return this.name }

  toJSON(): IFooSerialized {
      return {
          nameSomethingElse: this.name
      };
  }

  fromJSON(obj: IFooSerialized) {
        this.name = obj.nameSomethingElse;
  }
}

Then using it:

var foo1 = new Foo();
foo1.name = "John Doe";

var json = JSON.stringify(foo1);

json === '{"nameSomethingElse":"John Doe"}';

var foo2 = SerializationHelper.toInstance(new Foo(), json);

foo2.GetName() === "John Doe";

Solution 2: Base Class

Another way you could do this is by creating your own base class:

class Serializable {
    fillFromJSON(json: string) {
        var jsonObj = JSON.parse(json);
        for (var propName in jsonObj) {
            this[propName] = jsonObj[propName]
        }
    }
}

class Foo extends Serializable {
    name: string;
    GetName(): string { return this.name }
}

Then using it:

var foo = new Foo();
foo.fillFromJSON(json);

There's too many different ways to implement a custom deserialization using a base class so I'll leave that up to how you want it.

David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • Thank you. Both of these solutions seem a lot better than all the ones I've found before. Gonna give them a try a little later. – Klaus Apr 21 '15 at 08:01
  • 7
    @Klaus an issue with this is if you have nested objects. For example, if `Foo` had a property that was of type `Bar`. That problem is a bit more complex... – David Sherret Apr 21 '15 at 14:06
  • 1
    Damn. Nested objects was actually another issue I was about to ask at a later point. I feel like I don't know enough about TypeScript yet to be able to properly ask that follow up question. I wouldn't mind if you'd post a few key-words though that would allow me to look for solutions for the nested problem on my own though. Again, thanks already for your in depth answer above. – Klaus Apr 21 '15 at 14:16
  • To support nesting, could it be as simple as making the `fillFromJson` method recursive? I.e., your null check could determine whether the current `json` argument is an Object - if so, the recursion continues: `this[propName] = typeof jsonObj[propName] == 'object' ? fillFromJson(jsonObj[propName]) : jsonObj[propName]`. Have you tried this? I can try and see how it goes, I'm just wondering if you knew of any gotchas that could save me time. Thanks! – Danny Bullis Nov 13 '15 at 22:08
  • @DannyBullis I am wanting to come back and look at this problem in order to develop a better solution. Sometime in the future though. Until then, here's another way that does it with [recursion](http://stackoverflow.com/a/32169241/188246). Basically, you'll need to know the type of the properties too. – David Sherret Nov 13 '15 at 22:15
  • @DavidSherret thanks for the quick reply - yeah, I just looked at that post - that's how I ended up here :]. I had a question about that one too, I will post it there. Thanks again – Danny Bullis Nov 13 '15 at 22:20
62

You can now use Object.assign(target, ...sources). Following your example, you could use it like this:

class Foo {
  name: string;
  getName(): string { return this.name };
}

let fooJson: string = '{"name": "John Doe"}';
let foo: Foo = Object.assign(new Foo(), JSON.parse(fooJson));

console.log(foo.getName()); //returns John Doe

Object.assign is part of ECMAScript 2015 and is currently available in most modern browsers.

Hugo Leao
  • 841
  • 7
  • 8
  • 4
    In case the default constructor is missing on `Foo`, the target instance can be created through `Object.create(Foo.prototype)`, bypassing the constructor altogether. – Marcello Jul 26 '17 at 12:00
  • 27
    Does not work for nested objects – jzqa May 20 '18 at 06:06
  • @jzqa Can you give an example where it fails? I see its copying fine. – Ayyappa Jun 20 '20 at 15:22
  • 1
    Nested objects won't be mapped to their strong types when using normal `JSON` parsing. I suggest using a package like [class-transformer](https://www.npmjs.com/package/class-transformer) to properly map the objects and their types for any nested object. Their documentation is relatively easy to understand and get going as well – Johan Aspeling Jul 14 '21 at 14:47
19

What is actually the most robust and elegant automated solution for deserializing JSON to TypeScript runtime class instances?

Using property decorators with ReflectDecorators to record runtime-accessible type information that can be used during a deserialization process provides a surprisingly clean and widely adaptable approach, that also fits into existing code beautifully. It is also fully automatable, and works for nested objects as well.

An implementation of this idea is TypedJSON, which I created precisely for this task:

@JsonObject
class Foo {
    @JsonMember
    name: string;

    getName(): string { return this.name };
}
var foo = TypedJSON.parse('{"name": "John Doe"}', Foo);

foo instanceof Foo; // true
foo.getName(); // "John Doe"
John Weisz
  • 30,137
  • 13
  • 89
  • 132
  • 18
    If you want more people to use your library, I would recommend publishing on npm and fixing those typings.... with a package.json pointing to your index.d.ts file – David Jun 05 '16 at 20:30
  • Does this library work with React? – Keselme Jul 14 '20 at 09:36
  • 1
    @Keselme Yes, I myself use it in various projects where the view layer is implemented using React. – John Weisz Jul 14 '20 at 13:16
  • @JohnWeisz, I have a problem using it with React, I get an error that says `could not resolve detected property constructor at runtime.` I actually opened an issue could you take a look at it? https://github.com/JohnWeisz/TypedJSON/issues/130 – Keselme Jul 14 '20 at 13:19
4

Why could you not just do something like this?

class Foo {
  constructor(myObj){
     Object.assign(this, myObj);
  }
  get name() { return this._name; }
  set name(v) { this._name = v; }
}

let foo = new Foo({ name: "bat" });
foo.toJSON() //=> your json ...
amcdnl
  • 8,470
  • 12
  • 63
  • 99
-2

The best solution I found when dealing with Typescript classes and json objects: add a constructor in your Typescript class that takes the json data as parameter. In that constructor you extend your json object with jQuery, like this: $.extend( this, jsonData). $.extend allows keeping the javascript prototypes while adding the json object's properties.

export class Foo
{
    Name: string;
    getName(): string { return this.Name };

    constructor( jsonFoo: any )
    {
        $.extend( this, jsonFoo);
    }
}

In your ajax callback, translate your jsons in a your typescript object like this:

onNewFoo( jsonFoos : any[] )
{
  let receviedFoos = $.map( jsonFoos, (json) => { return new Foo( json ); } );

  // then call a method:
  let firstFooName = receviedFoos[0].GetName();
}

If you don't add the constructor, juste call in your ajax callback:

let newFoo = new Foo();
$.extend( newFoo, jsonData);
let name = newFoo.GetName()

...but the constructor will be useful if you want to convert the children json object too. See my detailed answer here.

Community
  • 1
  • 1
Anthony Brenelière
  • 60,646
  • 14
  • 46
  • 58