How do I initialize a TypeScript object with a JSO

2018-12-31 14:46发布

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!)

13条回答
低头抚发
2楼-- · 2018-12-31 15:24

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.

查看更多
泪湿衣
3楼-- · 2018-12-31 15:26

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
查看更多
余欢
4楼-- · 2018-12-31 15:29

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

查看更多
倾城一夜雪
5楼-- · 2018-12-31 15:33

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.

查看更多
墨雨无痕
6楼-- · 2018-12-31 15:35

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.

查看更多
孤独寂梦人
7楼-- · 2018-12-31 15:35

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
查看更多
登录 后发表回答