web-dev-qa-db-ja.com

TypeScriptオブジェクトをJSONオブジェクトで初期化する方法

AJAX呼び出しからRESTサーバーへのJSONオブジェクトを受け取りました。このオブジェクトは、私のTypeScriptクラスと一致するプロパティ名を持っています(これはこの質問の の続きです )。

それを初期化するための最良の方法は何ですか?クラス(&JSONオブジェクト)にはオブジェクトのリストであるメンバーとクラスであるメンバーがあり、それらのクラスにはリストやメンバーであるメンバーがあるため 、この は機能しないと思います。クラス。

しかし、私はメンバー名を調べてそれらに割り当て、リストを作成し、必要に応じてクラスをインスタンス化するアプローチを好むので、すべてのクラスのすべてのメンバーに対して明示的なコードを書く必要はありません(たくさんあります)

165
David Thielen

これは、いくつかの異なる方法を示すための簡単なショットです。彼らは決して「完全」ではありません、そして放棄として、私はそれがこのようにそれをするのは良い考えではないと思います。また、コードをすばやくまとめて入力しただけなので、コードはそれほどクリーンではありません。

また注として:もちろん、直列化復元可能クラスには、デフォルトのコンストラクターが必要です。他のすべての言語でそうであるように、直列化復元を認識している場合も同様です。もちろん、デフォルト以外のコンストラクタを引数なしで呼び出してもJavascriptは文句を言いませんが、その場合はクラスの準備をしておくほうがよいでしょう(さらに、それは実際には「タイプスクリプト的な方法」にはなりません)。

オプション1:実行時情報がまったくない

このアプローチの問題点は、ほとんどの場合、任意のメンバーの名前がそのクラスと一致しなければならないことです。これは自動的にクラスごとに同じ型の1つのメンバーにあなたを制限し、そして良い習慣のいくつかの規則を破ります。私はこれに対して強く忠告します、しかし私がこの答えを書いたときそれが最初の「ドラフト」だったのでそれをここにリストしてください(それは名前が「Foo」などである理由でもあります)。

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

オプション#2:nameプロパティ

オプション#1の問題を取り除くには、JSONオブジェクト内のノードがどのような型であるかに関するある種の情報が必要です。問題は、TypeScriptでは、これらはコンパイル時の構成要素であり、実行時に必要となることです。ただし、実行時オブジェクトは、設定されるまでそのプロパティを認識できません。

その方法の1つは、クラスに名前を認識させることです。ただし、JSONでもこのプロパティが必要です。実際には、あなたのみjsonでそれを必要とします。

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

    export class ExampleClass {
        private __= "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);

オプション3:メンバタイプを明示的に述べる

上で述べたように、クラスメンバの型情報は実行時には利用できません - それが利用可能にしない限りです。私たちは非原始的なメンバーのためにこれをする必要があるだけです、そして我々は行くのが良いです:

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

オプション4:冗長だがきれいな方法

Update 01/03/2016:TypeScript 1.7では、@ GameAlchemistがコメントで指摘したように、クラス/プロパティデコレータを使って以下の解決策を書くことができます。

シリアル化は常に問題であり、私の意見では、最善の方法は最短ではない方法です。すべてのオプションの中で、クラスの作成者が逆シリアル化されたオブジェクトの状態を完全に制御できるため、これが私の好みです。推測しなければならないのですが、遅かれ早かれ、他のすべての方法で問題が発生する可能性があります(Javascriptがこれに対処するためのネイティブな方法を考案しない限り)。

実際、次の例では柔軟性の正当性が確認されていません。それは本当にクラスの構造をコピーするだけです。ただし、ここで留意しなければならない違いは、クラスがクラス全体の状態を制御するために必要なあらゆる種類のJSONを使用するための完全な制御権を持っていることです(あなたはものを計算できます)。

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);
169
Ingo Bürk

TLDR: TypeSON (実用的な概念実証)


この問題の複雑さの根底にあるのは、runtimeにあるJSONをコンパイル時/にのみ逆シリアル化する必要があるということです。。これは型情報が実行時にどういうわけか利用可能にされることを必要とします。

幸い、これは decoratorsReflectDecorators を使って非常に洗練された堅牢な方法で解決できます。

  1. シリアル化の対象となるプロパティに プロパティデコレータ を使用して、メタデータ情報を記録し、その情報をどこかに保存します。たとえば、クラスのプロトタイプなどです。
  2. このメタデータ情報を再帰的イニシャライザ(デシリアライザ)に渡します

記録タイプ情報

ReflectDecorators とプロパティデコレータを組み合わせると、プロパティに関する型情報を簡単に記録できます。このアプローチの基本的な実装は次のようになります。

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

特定のプロパティについて、上記のスニペットは、プロパティのコンストラクタ関数の参照をクラスプロトタイプの隠し__propertyTypes__プロパティに追加します。例えば:

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

    @JsonMember// Number
    level: number;
}

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

    @JsonMember// Language
    language: Language;
}

これで、実行時に必要な型情報が完成しました。これを処理することができます。

処理タイプ情報

最初にJSON.parseを使用してObjectインスタンスを取得する必要があります - その後、__propertyTypes__内のエントリ全体を反復処理し、それに応じて必要なプロパティをインスタンス化できます。デシリアライザが開始点を持つように、ルートオブジェクトの型を指定する必要があります。

繰り返しますが、このアプローチの単純で単純な実装は次のようになります。

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

上記の考え方は、JSONにあるものの代わりに、expectedtypes(複素数/オブジェクト値)で逆シリアル化するという大きな利点があります。 Personが期待される場合、作成されるのはPersonインスタンスです。プリミティブ型と配列に対していくつかの追加のセキュリティ対策を講じれば、any悪意のあるJSONに抵抗するこのアプローチを安全にすることができます。

エッジケース

しかし、解決策がthatsimpleであることに満足しているのであれば、悪い知らせがあります。vast世話をする必要があるEdgeケースの数。そのうちのいくつかだけです:

  • 配列と配列要素(特にネストした配列の場合)
  • 多型
  • 抽象クラスとインタフェース
  • ...

これらすべてをいじる必要がないのであれば(そうではないと思いますが)、このアプローチを利用した概念実証の実用的な実験版をお勧めしたいと思います。 TypeSON - この問題に取り組むために私が作成した、毎日私が直面している問題。

デコレータがまだ実験的であると考えられている方法のために、私は生産的な使用のためにそれを使うことをお勧めしません、しかしこれまでのところそれは私によく役立ちました。

31
John Weisz

Object.assignを使用することができます。これがいつ追加されたのかはわかりませんが、現在TypeScript 2.0.2を使用しています。これはES6の機能のようです。

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

これはHalJsonです

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

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

export interface Link {
    readonly href: URL;
}

これがクロムが言うことです

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

だからあなたはそれが再帰的に代入をしないのを見ることができます

31
xenoterracide

私は仕事をするためにこの男を使っています: https://github.com/weichx/cerialize

とてもシンプルで強力です。それはサポートしています:

  • オブジェクトのツリー全体のシリアライゼーションとデシリアライゼーション.
  • 同じオブジェクトの永続的で一時的なプロパティ。
  • (デ)シリアル化ロジックをカスタマイズするためのフック。
  • それは(Angularに最適な)既存のインスタンスに(非)シリアライズすることも新しいインスタンスを生成することもできます。
  • 等.

例:

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);
11
André

オプション5:TypeScriptコンストラクタとjQuery.extendの使用

これは最も保守しやすい方法のようです。パラメータとしてjson構造をとるコンストラクタを追加し、jsonオブジェクトを拡張します。そうすれば、JSON構造をアプリケーションモデル全体に​​解析できます。

インタフェースを作成したり、コンストラクタにプロパティをリストしたりする必要はありません。

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

あなたが給料を計算するために会社を受け取るあなたのajaxコールバックで:

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

   // call the methods on your newCompany object ...
   newCompany.calculateSalaries()
}
3

TypeScriptインターフェイスを生成するツールと、JSON.parseの結果に対して実行時の型検査を実行するための実行時の "型マップ"を作成しました。 ts.quicktype.io

たとえば、次のJSONがあるとします。

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

quicktype は、次のTypeScriptインタフェースと型マップを生成します。

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

次に、型マップに対してJSON.parseの結果を確認します。

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

私はいくつかのコードを除外しましたが、詳細については quicktype を試すことができます。

3
David Siegel

単純なオブジェクトでは、私はこのメソッドが好きです。

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

コンストラクタでプロパティを定義する機能を利用すると、それを簡潔にすることができます。

これはあなたに型付けされたオブジェクトを(Object.assignやあなたにObjectを与える何らかの変種を使うすべての答えに対して)与え、外部のライブラリやデコレータを必要としません。

1
stevex

JQuery .extendがこれを行います。

var mytsobject = new mytsobject();

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

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

上記の4番目のオプションは、それを行うための単純で素晴らしい方法です。例えば、以下のサブクラスの出現のいずれかであるメンバーリストのようなクラス階層を処理しなければならない場合は2番目のオプションと組み合わせる必要があります。メンバーのスーパークラス、例えば、DirectorはMemberを拡張し、StudentはMemberを拡張します。その場合は、サブクラスの型をjson形式で指定する必要があります。

1
Xavier Méhaut

工場を使用する別の選択肢

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

こんな感じ

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. あなたのクラスをシンプルに保ちます
  2. 柔軟性のための工場に利用できる注入
0

この目的のために私が見つけた最高のものはクラス変圧器です。 github.com/typestack/class-transformer

それはあなたがそれをどのように使うかです:

いくつかのクラス:

export class Foo {

    name: string;

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

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


import { plainToClass } from 'class-transformer';

export class SomeService {

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

@Typeデコレータを使用すると、入れ子になったプロパティも作成されます。

0
Fabianus

実際のものではないかもしれませんが、単純な解決策は次のとおりです。

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

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

難しい依存関係でも働きます!!!