279

I receive a JSON object from an AJAX call to a REST server. This object has property names that match my TypeScript class (this is a follow-on to this question).

What is the best way to initialize it? I don't think this will work because the class (& JSON object) have members that are lists of objects and members that are classes, and those classes have members that are lists and/or classes.

But I'd prefer an approach that looks up the member names and assigns them across, creating lists and instantiating classes as needed, so I don't have to write explicit code for every member in every class (there's a LOT!)

leonheess
  • 16,068
  • 14
  • 77
  • 112
David Thielen
  • 28,723
  • 34
  • 119
  • 193
  • 1
    Why did you ask this again (as the answer I provided in the other question said this wouldn't work and that it was about copying properties into an existing object)? – WiredPrairie Apr 05 '14 at 20:45
  • 1
    possible duplicate of [How do I cast a JSON object to a typescript class](http://stackoverflow.com/questions/22875636/how-do-i-cast-a-json-object-to-a-typescript-class) – WiredPrairie Apr 05 '14 at 20:45
  • 3
    @WiredPrairie this question is different, it is asking if I can walk the properties one by one and assign them across. The other questions was asking if I could cast it. – David Thielen Apr 06 '14 at 17:04
  • 1
    @WiredPrairie cont: If you keep diving into properties until you get to just the primitive types, then those can be assigned across. – David Thielen Apr 06 '14 at 17:10
  • 2
    It's still copying all of the values just as I suggested you'd need to do. There's no new way to do this in TypeScript as it's a fundamental design of JavaScript. For large objects, you might not want to copy any values and just "act on" the data structure instead. – WiredPrairie Apr 06 '14 at 18:30

18 Answers18

221

These are some quick shots at this to show a few different ways. They are by no means "complete" and as a disclaimer, I don't think it's a good idea to do it like this. Also the code isn't too clean since I just typed it together rather quickly.

Also as a note: Of course deserializable classes need to have default constructors as is the case in all other languages where I'm aware of deserialization of any kind. Of course, Javascript won't complain if you call a non-default constructor with no arguments, but the class better be prepared for it then (plus, it wouldn't really be the "typescripty way").

Option #1: No run-time information at all

The problem with this approach is mostly that the name of any member must match its class. Which automatically limits you to one member of same type per class and breaks several rules of good practice. I strongly advise against this, but just list it here because it was the first "draft" when I wrote this answer (which is also why the names are "Foo" etc.).

module Environment {
    export class Sub {
        id: number;
    }

    export class Foo {
        baz: number;
        Sub: Sub;
    }
}

function deserialize(json, environment, clazz) {
    var instance = new clazz();
    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], environment, environment[prop]);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    baz: 42,
    Sub: {
        id: 1337
    }
};

var instance = deserialize(json, Environment, Environment.Foo);
console.log(instance);

Option #2: The name property

To get rid of the problem in option #1, we need to have some kind of information of what type a node in the JSON object is. The problem is that in Typescript, these things are compile-time constructs and we need them at runtime – but runtime objects simply have no awareness of their properties until they are set.

One way to do it is by making classes aware of their names. You need this property in the JSON as well, though. Actually, you only need it in the json:

module Environment {
    export class Member {
        private __name__ = "Member";
        id: number;
    }

    export class ExampleClass {
        private __name__ = "ExampleClass";

        mainId: number;
        firstMember: Member;
        secondMember: Member;
    }
}

function deserialize(json, environment) {
    var instance = new environment[json.__name__]();
    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], environment);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    __name__: "ExampleClass",
    mainId: 42,
    firstMember: {
        __name__: "Member",
        id: 1337
    },
    secondMember: {
        __name__: "Member",
        id: -1
    }
};

var instance = deserialize(json, Environment);
console.log(instance);

Option #3: Explicitly stating member types

As stated above, the type information of class members is not available at runtime – that is unless we make it available. We only need to do this for non-primitive members and we are good to go:

interface Deserializable {
    getTypes(): Object;
}

class Member implements Deserializable {
    id: number;

    getTypes() {
        // since the only member, id, is primitive, we don't need to
        // return anything here
        return {};
    }
}

class ExampleClass implements Deserializable {
    mainId: number;
    firstMember: Member;
    secondMember: Member;

    getTypes() {
        return {
            // this is the duplication so that we have
            // run-time type information :/
            firstMember: Member,
            secondMember: Member
        };
    }
}

function deserialize(json, clazz) {
    var instance = new clazz(),
        types = instance.getTypes();

    for(var prop in json) {
        if(!json.hasOwnProperty(prop)) {
            continue;
        }

        if(typeof json[prop] === 'object') {
            instance[prop] = deserialize(json[prop], types[prop]);
        } else {
            instance[prop] = json[prop];
        }
    }

    return instance;
}

var json = {
    mainId: 42,
    firstMember: {
        id: 1337
    },
    secondMember: {
        id: -1
    }
};

var instance = deserialize(json, ExampleClass);
console.log(instance);

Option #4: The verbose, but neat way

Update 01/03/2016: As @GameAlchemist pointed out in the comments (idea, implementation), as of Typescript 1.7, the solution described below can be written in a better way using class/property decorators.

Serialization is always a problem and in my opinion, the best way is a way that just isn't the shortest. Out of all the options, this is what I'd prefer because the author of the class has full control over the state of deserialized objects. If I had to guess, I'd say that all other options, sooner or later, will get you in trouble (unless Javascript comes up with a native way for dealing with this).

Really, the following example doesn't do the flexibility justice. It really does just copy the class's structure. The difference you have to keep in mind here, though, is that the class has full control to use any kind of JSON it wants to control the state of the entire class (you could calculate things etc.).

interface Serializable<T> {
    deserialize(input: Object): T;
}

class Member implements Serializable<Member> {
    id: number;

    deserialize(input) {
        this.id = input.id;
        return this;
    }
}

class ExampleClass implements Serializable<ExampleClass> {
    mainId: number;
    firstMember: Member;
    secondMember: Member;

    deserialize(input) {
        this.mainId = input.mainId;

        this.firstMember = new Member().deserialize(input.firstMember);
        this.secondMember = new Member().deserialize(input.secondMember);

        return this;
    }
}

var json = {
    mainId: 42,
    firstMember: {
        id: 1337
    },
    secondMember: {
        id: -1
    }
};

var instance = new ExampleClass().deserialize(json);
console.log(instance);
ruffin
  • 16,507
  • 9
  • 88
  • 138
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
  • Thank you. I think you're right that I'm probably better off writing explicit constructors although option 4 is really slick. I'm still learning the different world of javascript (my world has been java & C# for a long time). – David Thielen Apr 06 '14 at 17:08
  • 15
    Option #4 is what I'd call a reasonable way to go. You still need to write the deserialization code, but it's in the same class and fully controllable. If you're coming from Java, then this is comparable to having to write `equals` or `toString` methods (only that you usually have them auto-generated). It shouldn't be *too* hard to write a generator for `deserialize` if you wanted to, but it just can't be run-time automation. – Ingo Bürk Apr 06 '14 at 17:11
  • 4
    @IngoBürk, I know I am asking this question 2 years later but how will this work on array of objects? The sample code above works fine for JSON object. how can it be used for array of objects? – Pratik Gaikwad Jan 12 '16 at 20:22
  • Also how to use it with class in which constructor is defined? – Pratik Gaikwad Jan 12 '16 at 20:28
  • @PratikGaikwad I don't see why you couldn't make it work for arrays as well. And in deserialization you generally don't care about constructors. The serialized stream already contains the constructed object. – Ingo Bürk Jan 12 '16 at 20:33
  • @IngoBürk, I am kind of newbie to Typescript coding. If you could help me with array's that would be a lot help. and as for constructor question, what if I want to create a object with some values apart from JSON data, how can I achieve that? – Pratik Gaikwad Jan 12 '16 at 20:43
  • @PratikGaikwad If you want to create an object from an incomplete JSON object, then you have broken serialization data. Serialization should contain everything the object needs. Anywho, you can do anything you want in the deserialize function, including setting any other fields. Just seems like a odd concept to me. – Ingo Bürk Jan 12 '16 at 22:10
  • @IngoBürk, you misunderstood my question. I am not breaking serialization process. What I am saying is from array of Json objects, using deserialization process mentioned above, I have created number of objects. After that during runtime I need to create more object(s) from another piece of code. For that I will need a paramatrised constructor,correct? Since we can not have constructor overloading, how to achieve both the things simultaneously? – Pratik Gaikwad Jan 13 '16 at 03:25
  • 3
    A side remark : since the 1.7, (admittedly more recent than your answer), typescript provides class/property decorators that allows to write the 4th solution in a neater way. – GameAlchemist Jan 14 '16 at 09:20
  • @GameAlchemist Care to expand on that ? How can i find more info on those ? – Growiel Feb 23 '16 at 10:03
  • 2
    The best documentation i found is a StackOverflow answer : http://stackoverflow.com/a/29837695/856501 . I used decorators in a project of mine, and although i'd like a few other features i have to say they work like a charm. – GameAlchemist Feb 23 '16 at 10:08
  • @GameAlchemist this blog post is very interesting as well : http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4, and would allow to write a simple generic deserializer if we had true reflection API support in TypeScript – Frédéric Camblor Mar 17 '16 at 10:43
  • @Frédéric, maybe one of you can provide another solution with reflection support? – storm_buster Apr 01 '16 at 14:54
  • 2
    I wouldn't jump on decorators for a production project just yet - keep in mind they are still an experimental feature. I wouldn't base real-world code on "experiments" because as far as we are concerned they may be gone in the next version and you'd have to rewrite a bunch of code or be forever stuck on an old TS version. Just my $.02 – RVP Apr 19 '16 at 15:39
  • @GameAlchemist It's not nearly as easy with decorators as one might expect. There are countless edge cases, some of which are: polymorphic properties, inherited properties, arrays, nested arrays. The last two are especially tricky, as the TypeScript compiler [will not emit metadata for array elements](http://stackoverflow.com/q/35022658/2788872). Also, polymorhpism will require some kind of type-annotation to be present in the source JSON (that's how both Json.NET and data contracts do it in C#). – John Weisz Apr 28 '16 at 19:40
  • @Ingo Bürk Using Option #2 gives me: "Cannot use namespace 'environment' as a value." Any solution for this? – Xoyce Aug 29 '17 at 11:50
  • @PratikGaikwad Just FYI, I created a solution for Option 4 here: https://stackoverflow.com/questions/49163375/typeerror-currentpizzaplace-getpoint-is-not-a-function/49175847#49175847 that deserializes an array of objects. Hope this helps. – Thom Mar 08 '18 at 14:45
61

you can use Object.assign I don't know when this was added, I'm currently using Typescript 2.0.2, and this appears to be an ES6 feature.

client.fetch( '' ).then( response => {
        return response.json();
    } ).then( json => {
        let hal : HalJson = Object.assign( new HalJson(), json );
        log.debug( "json", hal );

here's HalJson

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

export interface Links {
    readonly [text: string]: Link;
}

export interface Link {
    readonly href: URL;
}

here's what chrome says it is

HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public

so you can see it doesn't do the assign recursively

xenoterracide
  • 16,274
  • 24
  • 118
  • 243
  • 2
    so, basically, it's this: `Object.assign`. Why do we have two lexicon-like answers above this one then? – phil294 Dec 18 '16 at 00:57
  • 27
    @Blauhim Because `Object.assign` will not work recursively, and not instantiate correct object types, leaving values as `Object` instances. While it is fine for trivial tasks, complex type serialization is not possible with it. For example, if a class property is of a custom class type, `JSON.parse` + `Object.assign` will instantiate that property to `Object`. Side-effects include missing methods and accessors. – John Weisz Jan 04 '17 at 14:32
  • @JohnWeisz the top level class of object assign does have the correct type, and I mentioned the recursive thing in this... that said, YMMV, and those might be deal breakers. – xenoterracide Jan 05 '17 at 02:53
  • Quoted directly from the question: *"the class have members that are lists of objects and members that are classes, and those classes have members that are lists and/or classes [...] I'd prefer an approach that looks up the member names and assigns them across, creating lists and instantiating classes as needed, **so I don't have to write explicit code for every member in every class**"* -- which is not the case with `Object.assign`, where it still comes down to writing nested instantiation by hand. This approach is fine for very simple, tutorial-level objects, but not for real use. – John Weisz Jan 09 '17 at 15:30
  • @JohnWeisz sure, mostly answered with this because it wasn't in any answers and seemed simple for some use cases. I'm certain it could also be used in combination with other answers such as reflection, in order to do what you're looking for. I also wrote it in part so that I would remember it later. Looking at these answers and having used and written much more powerful libraries there doesn't appear to be anything available for "real use". – xenoterracide Jan 09 '17 at 15:40
45

TLDR: TypedJSON (working proof of concept)


The root of the complexity of this problem is that we need to deserialize JSON at runtime using type information that only exists at compile time. This requires that type-information is somehow made available at runtime.

Fortunately, this can be solved in a very elegant and robust way with decorators and ReflectDecorators:

  1. Use property decorators on properties which are subject to serialization, to record metadata information and store that information somewhere, for example on the class prototype
  2. Feed this metadata information to a recursive initializer (deserializer)

 

Recording Type-Information

With a combination of ReflectDecorators and property decorators, type information can be easily recorded about a property. A rudimentary implementation of this approach would be:

function JsonMember(target: any, propertyKey: string) {
    var metadataFieldKey = "__propertyTypes__";

    // Get the already recorded type-information from target, or create
    // empty object if this is the first property.
    var propertyTypes = target[metadataFieldKey] || (target[metadataFieldKey] = {});

    // Get the constructor reference of the current property.
    // This is provided by TypeScript, built-in (make sure to enable emit
    // decorator metadata).
    propertyTypes[propertyKey] = Reflect.getMetadata("design:type", target, propertyKey);
}

For any given property, the above snippet will add a reference of the constructor function of the property to the hidden __propertyTypes__ property on the class prototype. For example:

class Language {
    @JsonMember // String
    name: string;

    @JsonMember// Number
    level: number;
}

class Person {
    @JsonMember // String
    name: string;

    @JsonMember// Language
    language: Language;
}

And that's it, we have the required type-information at runtime, which can now be processed.

 

Processing Type-Information

We first need to obtain an Object instance using JSON.parse -- after that, we can iterate over the entires in __propertyTypes__ (collected above) and instantiate the required properties accordingly. The type of the root object must be specified, so that the deserializer has a starting-point.

Again, a dead simple implementation of this approach would be:

function deserialize<T>(jsonObject: any, Constructor: { new (): T }): T {
    if (!Constructor || !Constructor.prototype.__propertyTypes__ || !jsonObject || typeof jsonObject !== "object") {
        // No root-type with usable type-information is available.
        return jsonObject;
    }

    // Create an instance of root-type.
    var instance: any = new Constructor();

    // For each property marked with @JsonMember, do...
    Object.keys(Constructor.prototype.__propertyTypes__).forEach(propertyKey => {
        var PropertyType = Constructor.prototype.__propertyTypes__[propertyKey];

        // Deserialize recursively, treat property type as root-type.
        instance[propertyKey] = deserialize(jsonObject[propertyKey], PropertyType);
    });

    return instance;
}
var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }';
var person: Person = deserialize(JSON.parse(json), Person);

The above idea has a big advantage of deserializing by expected types (for complex/object values), instead of what is present in the JSON. If a Person is expected, then it is a Person instance that is created. With some additional security measures in place for primitive types and arrays, this approach can be made secure, that resists any malicious JSON.

 

Edge Cases

However, if you are now happy that the solution is that simple, I have some bad news: there is a vast number of edge cases that need to be taken care of. Only some of which are:

  • Arrays and array elements (especially in nested arrays)
  • Polymorphism
  • Abstract classes and interfaces
  • ...

If you don't want to fiddle around with all of these (I bet you don't), I'd be glad to recommend a working experimental version of a proof-of-concept utilizing this approach, TypedJSON -- which I created to tackle this exact problem, a problem I face myself daily.

Due to how decorators are still being considered experimental, I wouldn't recommend using it for production use, but so far it served me well.

John Weisz
  • 30,137
  • 13
  • 89
  • 132
  • TypedJSON worked great; thanks a lot for the reference. – Neil Nov 09 '16 at 07:04
  • Great work, you've come up with a very elegant solution to a problem that has been troubling me for a while. I'll be following your project very closely ! – John Strickler Jan 09 '17 at 15:24
20

I've created a tool that generates TypeScript interfaces and a runtime "type map" for performing runtime typechecking against the results of JSON.parse: ts.quicktype.io

For example, given this JSON:

{
  "name": "David",
  "pets": [
    {
      "name": "Smoochie",
      "species": "rhino"
    }
  ]
}

quicktype produces the following TypeScript interface and type map:

export interface Person {
    name: string;
    pets: Pet[];
}

export interface Pet {
    name:    string;
    species: string;
}

const typeMap: any = {
    Person: {
        name: "string",
        pets: array(object("Pet")),
    },
    Pet: {
        name: "string",
        species: "string",
    },
};

Then we check the result of JSON.parse against the type map:

export function fromJson(json: string): Person {
    return cast(JSON.parse(json), object("Person"));
}

I've left out some code, but you can try quicktype for the details.

Doguhan Uluca
  • 6,933
  • 4
  • 38
  • 51
David Siegel
  • 1,604
  • 11
  • 13
  • 1
    After doing many hours of research and trying my hand at a couple of parsing techniques, I can say this is an excellent solution -- mainly because decorators are still experimental. * The original link is broken for me; but http://ts.quicktype.io works. * Converting the JSON to JSON Schema is a good first step. – LexH Jul 29 '18 at 05:07
12

I've been using this guy to do the job: https://github.com/weichx/cerialize

It's very simple yet powerful. It supports:

  • Serialization & deserialization of a whole tree of objects.
  • Persistent & transient properties on the same object.
  • Hooks to customize the (de)serialization logic.
  • It can (de)serialize into an existing instance (great for Angular) or generate new instances.
  • etc.

Example:

class Tree {
  @deserialize public species : string; 
  @deserializeAs(Leaf) public leafs : Array<Leaf>;  //arrays do not need extra specifications, just a type.
  @deserializeAs(Bark, 'barkType') public bark : Bark;  //using custom type and custom key name
  @deserializeIndexable(Leaf) public leafMap : {[idx : string] : Leaf}; //use an object as a map
}

class Leaf {
  @deserialize public color : string;
  @deserialize public blooming : boolean;
  @deserializeAs(Date) public bloomedAt : Date;
}

class Bark {
  @deserialize roughness : number;
}

var json = {
  species: 'Oak',
  barkType: { roughness: 1 },
  leafs: [ {color: 'red', blooming: false, bloomedAt: 'Mon Dec 07 2015 11:48:20 GMT-0500 (EST)' } ],
  leafMap: { type1: { some leaf data }, type2: { some leaf data } }
}
var tree: Tree = Deserialize(json, Tree);
André
  • 12,497
  • 6
  • 42
  • 44
8

For simple objects, I like this method:

class Person {
  constructor(
    public id: String, 
    public name: String, 
    public title: String) {};

  static deserialize(input:any): Person {
    return new Person(input.id, input.name, input.title);
  }
}

var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});

Leveraging the ability to define properties in the constructor lets it be concise.

This gets you a typed object (vs all the answers that use Object.assign or some variant, which give you an Object) and doesn't require external libraries or decorators.

stevex
  • 5,589
  • 37
  • 52
6

This is my approach (very simple):

const jsonObj: { [key: string]: any } = JSON.parse(jsonStr);

for (const key in jsonObj) {
  if (!jsonObj.hasOwnProperty(key)) {
    continue;
  }

  console.log(key); // Key
  console.log(jsonObj[key]); // Value
  // Your logic...
}
KimchiMan
  • 4,836
  • 6
  • 35
  • 41
4

if you want type safety and don't like decorators

abstract class IPerson{
  name?: string;
  age?: number;
}
class Person extends IPerson{
  constructor({name, age}: IPerson){
    super();
    this.name = name;
    this.age = age;
  }
}

const json = {name: "ali", age: 80};
const person = new Person(json);

or this which I prefer

class Person {
  constructor(init?: Partial<Person>){
      Object.assign(this, init);
  }
  name?: string;
  age?: number;
}

const json = {name: "ali", age: 80};
const person = new Person(json);
Ali80
  • 6,333
  • 2
  • 43
  • 33
2

Option #5: Using Typescript constructors and jQuery.extend

This seems to be the most maintainable method: add a constructor that takes as parameter the json structure, and extend the json object. That way you can parse a json structure into the whole application model.

There is no need to create interfaces, or listing properties in constructor.

export class Company
{
    Employees : Employee[];

    constructor( jsonData: any )
    {
        jQuery.extend( this, jsonData);

        // apply the same principle to linked objects:
        if ( jsonData.Employees )
            this.Employees = jQuery.map( jsonData.Employees , (emp) => {
                return new Employee ( emp );  });
    }

    calculateSalaries() : void { .... }
}

export class Employee
{
    name: string;
    salary: number;
    city: string;

    constructor( jsonData: any )
    {
        jQuery.extend( this, jsonData);

        // case where your object's property does not match the json's:
        this.city = jsonData.town;
    }
}

In your ajax callback where you receive a company to calculate salaries:

onReceiveCompany( jsonCompany : any ) 
{
   let newCompany = new Company( jsonCompany );

   // call the methods on your newCompany object ...
   newCompany.calculateSalaries()
}
Anthony Brenelière
  • 60,646
  • 14
  • 46
  • 58
2

The best I found for this purpose is the class-transformer

That's how you use it:

Some class:

export class Foo {

    name: string;

    @Type(() => Bar)
    bar: Bar;

    public someFunction = (test: string): boolean => {
        ...
    }
}

// the docs say "import [this shim] in a global place, like app.ts" 
import 'reflect-metadata';

// import this function where you need to use it
import { plainToClass } from 'class-transformer';

export class SomeService {

  anyFunction() {
    u = plainToClass(Foo, JSONobj);
  }
}

If you use the @Type decorator nested properties will be created, too.

John Cummings
  • 1,949
  • 3
  • 22
  • 38
Fabianus
  • 633
  • 7
  • 16
  • See also the comments on a [similar answer](https://stackoverflow.com/a/40042282/1518546) to the related question the OP references. – John Cummings Jul 15 '22 at 22:11
1

The 4th option described above is a simple and nice way to do it, which has to be combined with the 2nd option in the case where you have to handle a class hierarchy like for instance a member list which is any of a occurences of subclasses of a Member super class, eg Director extends Member or Student extends Member. In that case you have to give the subclass type in the json format

1

JQuery .extend does this for you:

var mytsobject = new mytsobject();

var newObj = {a:1,b:2};

$.extend(mytsobject, newObj); //mytsobject will now contain a & b
Daniel
  • 506
  • 2
  • 5
1

Another option using factories

export class A {

    id: number;

    date: Date;

    bId: number;
    readonly b: B;
}

export class B {

    id: number;
}

export class AFactory {

    constructor(
        private readonly createB: BFactory
    ) { }

    create(data: any): A {

        const createB = this.createB.create;

        return Object.assign(new A(),
            data,
            {
                get b(): B {

                    return createB({ id: data.bId });
                },
                date: new Date(data.date)
            });
    }
}

export class BFactory {

    create(data: any): B {

        return Object.assign(new B(), data);
    }
}

https://github.com/MrAntix/ts-deserialize

use like this

import { A, B, AFactory, BFactory } from "./deserialize";

// create a factory, simplified by DI
const aFactory = new AFactory(new BFactory());

// get an anon js object like you'd get from the http call
const data = { bId: 1, date: '2017-1-1' };

// create a real model from the anon js object
const a = aFactory.create(data);

// confirm instances e.g. dates are Dates 
console.log('a.date is instanceof Date', a.date instanceof Date);
console.log('a.b is instanceof B', a.b instanceof B);
  1. keeps your classes simple
  2. injection available to the factories for flexibility
Anthony Johnston
  • 9,405
  • 4
  • 46
  • 57
1

I personally prefer option #3 of @Ingo Bürk. And I improved his codes to support an array of complex data and Array of primitive data.

interface IDeserializable {
  getTypes(): Object;
}

class Utility {
  static deserializeJson<T>(jsonObj: object, classType: any): T {
    let instanceObj = new classType();
    let types: IDeserializable;
    if (instanceObj && instanceObj.getTypes) {
      types = instanceObj.getTypes();
    }

    for (var prop in jsonObj) {
      if (!(prop in instanceObj)) {
        continue;
      }

      let jsonProp = jsonObj[prop];
      if (this.isObject(jsonProp)) {
        instanceObj[prop] =
          types && types[prop]
            ? this.deserializeJson(jsonProp, types[prop])
            : jsonProp;
      } else if (this.isArray(jsonProp)) {
        instanceObj[prop] = [];
        for (let index = 0; index < jsonProp.length; index++) {
          const elem = jsonProp[index];
          if (this.isObject(elem) && types && types[prop]) {
            instanceObj[prop].push(this.deserializeJson(elem, types[prop]));
          } else {
            instanceObj[prop].push(elem);
          }
        }
      } else {
        instanceObj[prop] = jsonProp;
      }
    }

    return instanceObj;
  }

  //#region ### get types ###
  /**
   * check type of value be string
   * @param {*} value
   */
  static isString(value: any) {
    return typeof value === "string" || value instanceof String;
  }

  /**
   * check type of value be array
   * @param {*} value
   */
  static isNumber(value: any) {
    return typeof value === "number" && isFinite(value);
  }

  /**
   * check type of value be array
   * @param {*} value
   */
  static isArray(value: any) {
    return value && typeof value === "object" && value.constructor === Array;
  }

  /**
   * check type of value be object
   * @param {*} value
   */
  static isObject(value: any) {
    return value && typeof value === "object" && value.constructor === Object;
  }

  /**
   * check type of value be boolean
   * @param {*} value
   */
  static isBoolean(value: any) {
    return typeof value === "boolean";
  }
  //#endregion
}

// #region ### Models ###
class Hotel implements IDeserializable {
  id: number = 0;
  name: string = "";
  address: string = "";
  city: City = new City(); // complex data
  roomTypes: Array<RoomType> = []; // array of complex data
  facilities: Array<string> = []; // array of primitive data

  // getter example
  get nameAndAddress() {
    return `${this.name} ${this.address}`;
  }

  // function example
  checkRoom() {
    return true;
  }

  // this function will be use for getting run-time type information
  getTypes() {
    return {
      city: City,
      roomTypes: RoomType
    };
  }
}

class RoomType implements IDeserializable {
  id: number = 0;
  name: string = "";
  roomPrices: Array<RoomPrice> = [];

  // getter example
  get totalPrice() {
    return this.roomPrices.map(x => x.price).reduce((a, b) => a + b, 0);
  }

  getTypes() {
    return {
      roomPrices: RoomPrice
    };
  }
}

class RoomPrice {
  price: number = 0;
  date: string = "";
}

class City {
  id: number = 0;
  name: string = "";
}
// #endregion

// #region ### test code ###
var jsonObj = {
  id: 1,
  name: "hotel1",
  address: "address1",
  city: {
    id: 1,
    name: "city1"
  },
  roomTypes: [
    {
      id: 1,
      name: "single",
      roomPrices: [
        {
          price: 1000,
          date: "2020-02-20"
        },
        {
          price: 1500,
          date: "2020-02-21"
        }
      ]
    },
    {
      id: 2,
      name: "double",
      roomPrices: [
        {
          price: 2000,
          date: "2020-02-20"
        },
        {
          price: 2500,
          date: "2020-02-21"
        }
      ]
    }
  ],
  facilities: ["facility1", "facility2"]
};

var hotelInstance = Utility.deserializeJson<Hotel>(jsonObj, Hotel);

console.log(hotelInstance.city.name);
console.log(hotelInstance.nameAndAddress); // getter
console.log(hotelInstance.checkRoom()); // function
console.log(hotelInstance.roomTypes[0].totalPrice); // getter
// #endregion

0

you can do like below

export interface Instance {
  id?:string;
  name?:string;
  type:string;
}

and

var instance: Instance = <Instance>({
      id: null,
      name: '',
      type: ''
    });
Md Ayub Ali Sarker
  • 10,795
  • 4
  • 24
  • 19
  • 2
    This will not actually produce a runtime instance of your expected object type. It will appear to work when your type only has primitive properties, but will fail when a type has methods. Interface definitions are also not available at runtime (only build time). – Todd Jan 22 '17 at 03:40
0

My approach is slightly different. I do not copy properties into new instances, I just change the prototype of existing POJOs (may not work well on older browsers). Each class is responsible for providing a SetPrototypes method to set the prototoypes of any child objects, which in turn provide their own SetPrototypes methods.

(I also use a _Type property to get the class name of unknown objects but that can be ignored here)

class ParentClass
{
    public ID?: Guid;
    public Child?: ChildClass;
    public ListOfChildren?: ChildClass[];

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ParentClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
        ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
    }
}

class ChildClass
{
    public ID?: Guid;
    public GrandChild?: GrandChildClass;

    /**
     * Set the prototypes of all objects in the graph.
     * Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
     * @param pojo Plain object received from API/JSON to be given the class prototype.
     */
    private static SetPrototypes(pojo: ChildClass): void
    {
        ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
    }
}

Here is ObjectUtils.ts:

/**
 * ClassType lets us specify arguments as class variables.
 * (where ClassType == window[ClassName])
 */
type ClassType = { new(...args: any[]): any; };

/**
 * The name of a class as opposed to the class itself.
 * (where ClassType == window[ClassName])
 */
type ClassName = string & {};

abstract class ObjectUtils
{
/**
 * Set the prototype of an object to the specified class.
 *
 * Does nothing if source or type are null.
 * Throws an exception if type is not a known class type.
 *
 * If type has the SetPrototypes method then that is called on the source
 * to perform recursive prototype assignment on an object graph.
 *
 * SetPrototypes is declared private on types because it should only be called
 * by this method. It does not (and must not) set the prototype of the object
 * itself - only the protoypes of child properties, otherwise it would cause a
 * loop. Thus a public method would be misleading and not useful on its own.
 * 
 * https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
 */
public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
{
    let classType = (typeof type === "string") ? window[type] : type;

    if (!source || !classType)
    {
        return source;
    }

    // Guard/contract utility
    ExGuard.IsValid(classType.prototype, "type", <any>type);

    if ((<any>Object).setPrototypeOf)
    {
        (<any>Object).setPrototypeOf(source, classType.prototype);
    }
    else if (source.__proto__)
    {
        source.__proto__ = classType.prototype.__proto__;
    }

    if (typeof classType["SetPrototypes"] === "function")
    {
        classType["SetPrototypes"](source);
    }

    return source;
}

/**
 * Set the prototype of a list of objects to the specified class.
 * 
 * Throws an exception if type is not a known class type.
 */
public static SetPrototypeOfAll(source: any[], type: ClassType): void
{
    if (!source)
    {
        return;
    }

    for (var i = 0; i < source.length; i++)
    {
        this.SetPrototypeOf(source[i], type);
    }
}
}

Usage:

let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;

let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);

// parentObject is now a proper ParentClass instance
Etherman
  • 1,777
  • 1
  • 21
  • 34
-1

Maybe not actual, but simple solution:

interface Bar{
x:number;
y?:string; 
}

var baz:Bar = JSON.parse(jsonString);
alert(baz.y);

work for difficult dependencies too!!!

ArK
  • 20,698
  • 67
  • 109
  • 136
  • 10
    This approach does not actually work as expected. If you inspect the runtime results, `baz` will be of type `Object` and not type `Bar.` It works in this simple case because `Bar` has no methods (just primitive properties). If `Bar` had a method like `isEnabled()`, this approach would fail since that method would not be in the serialized JSON string. – Todd Jan 22 '17 at 03:39
-1
**model.ts**
export class Item {
    private key: JSON;
    constructor(jsonItem: any) {
        this.key = jsonItem;
    }
}

**service.ts**
import { Item } from '../model/items';

export class ItemService {
    items: Item;
    constructor() {
        this.items = new Item({
            'logo': 'Logo',
            'home': 'Home',
            'about': 'About',
            'contact': 'Contact',
        });
    }
    getItems(): Item {
        return this.items;
    }
}