webentwicklung-frage-antwort-db.com.de

Wie initialisiere ich ein TypeScript-Objekt mit einem JSON-Objekt?

Ich erhalte ein JSON-Objekt von einem AJAX - Aufruf an einen REST -Server. Dieses Objekt hat Eigenschaftsnamen, die mit meiner TypeScript-Klasse übereinstimmen (dies ist eine Folge von dieser Frage ).

Wie kann ich es am besten initialisieren? Ich glaube nicht, dass this funktionieren wird, weil die Klasse (& JSON-Objekt) Mitglieder hat, die Listen von Objekten und Mitglieder sind, die Klassen sind, und diese Klassen Mitglieder haben, die Listen und/oder Klassen sind.

Aber ich würde einen Ansatz bevorzugen, der die Namen der Mitglieder nachschlägt und sie einander zuordnet, Listen erstellt und Klassen nach Bedarf instanziiert, sodass ich nicht für jedes Mitglied in jeder Klasse expliziten Code schreiben muss (es gibt eine Menge!)

155
David Thielen

Dies sind einige schnelle Einstellungen, um einige Möglichkeiten aufzuzeigen. Sie sind keinesfalls "vollständig" und als Haftungsausschluss halte ich es nicht für eine gute Idee, dies so zu tun. Auch der Code ist nicht zu sauber, da ich ihn nur ziemlich schnell zusammen eingegeben habe.

Auch als Anmerkung: Natürlich müssen deserialisierbare Klassen über Standardkonstruktoren verfügen, wie dies in allen anderen Sprachen der Fall ist, in denen mir jegliche Deserialisierung bekannt ist. Natürlich beschwert sich Javascript nicht, wenn Sie einen Nicht-Standardkonstruktor ohne Argumente aufrufen, aber die Klasse sollte sich darauf vorbereiten (außerdem wäre es nicht die "Typoskriptionsmethode").

Option # 1: Keine Laufzeitinformationen

Das Problem bei diesem Ansatz ist meistens, dass der Name eines Mitglieds seiner Klasse entsprechen muss. Dadurch werden Sie automatisch auf ein Mitglied desselben Typs pro Klasse beschränkt und verstoßen gegen einige Regeln der guten Praxis. Ich rate ausdrücklich davon ab, aber listen Sie es hier einfach auf, weil es der erste "Entwurf" war, als ich diese Antwort schrieb (weshalb die Namen auch "Foo" usw. sind).

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: Die name -Eigenschaft

Um das Problem in Option Nr. 1 zu beseitigen, benötigen wir Informationen darüber, welcher Typ ein Knoten im JSON-Objekt ist. Das Problem ist, dass diese Dinge in TypeScript Kompilierungszeitkonstrukte sind, die wir zur Laufzeit benötigen - aber Laufzeitobjekte haben einfach keine Kenntnis ihrer Eigenschaften, bis sie festgelegt werden.

Eine Möglichkeit, dies zu tun, besteht darin, den Klassen ihre Namen bewusst zu machen. Sie benötigen diese Eigenschaft jedoch auch in der JSON. Eigentlich brauchen Sie nur es im 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);

Option # 3: Elementtypen explizit angeben

Wie bereits erwähnt, sind die Typinformationen der Klassenmitglieder zur Laufzeit nicht verfügbar - das heißt, wenn wir sie nicht zur Verfügung stellen. Wir müssen dies nur für nicht-primitive Mitglieder tun, und wir sind gut zu gehen:

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: Die wortreiche, aber saubere Weise

Update 01/03/2016: Wie @GameAlchemist in den Kommentaren bereits seit TypeScript 1.7 darauf hingewiesen hat, kann die im Folgenden beschriebene Lösung mit Klassen-/Eigenschaftendekoratoren besser geschrieben werden.

Die Serialisierung ist immer ein Problem und meiner Meinung nach ist der beste Weg ein Weg, der nicht der kürzeste ist. Unter all diesen Optionen würde ich dies vorziehen, da der Autor der Klasse die vollständige Kontrolle über den Zustand deserialisierter Objekte hat. Wenn ich raten müsste, würde ich sagen, dass alle anderen Optionen früher oder später in Schwierigkeiten geraten werden (es sei denn, Javascript bietet eine native Methode, um damit umzugehen).

Wirklich, das folgende Beispiel wird der Flexibilität nicht gerecht. Es kopiert wirklich nur die Struktur der Klasse. Der Unterschied, den Sie dabei beachten müssen, ist, dass die Klasse die vollständige Kontrolle hat, um jede Art von JSON zu verwenden, die den Status der gesamten Klasse steuern soll (Sie könnten Dinge berechnen usw.).

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

TLDR: TypedJSON (funktionierender Proof of Concept)


Die Wurzel der Komplexität dieses Problems ist, dass wir JSON um Laufzeit unter Verwendung von Typinformationen deserialisieren müssen, die nur um Kompilierzeit vorhanden sind. Dies setzt voraus, dass Typinformationen zur Laufzeit irgendwie zur Verfügung gestellt werden.

Zum Glück kann dies mit Dekoratoren und ReflectDecorators sehr elegant und robust gelöst werden: /:

  1. Verwenden Sie property decorators on Eigenschaften, die der Serialisierung unterliegen, um Metadateninformationen aufzuzeichnen und diese Informationen irgendwo zu speichern, beispielsweise im Klassenprototyp
  2. Diese Metadateninformationen einem rekursiven Initialisierer (Deserializer) zuführen

Aufnahmetyp-Informationen

Mit einer Kombination von ReflectDecorators und Eigenschaftendekoratoren können Typinformationen leicht über eine Eigenschaft aufgezeichnet werden. Eine rudimentäre Umsetzung dieses Ansatzes wäre:

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

Für jede gegebene Eigenschaft fügt das obige Snippet der versteckten __propertyTypes__ -Eigenschaft des Klassenprototyps einen Verweis auf die Konstruktorfunktion der Eigenschaft hinzu. Zum Beispiel:

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

    @JsonMember// Number
    level: number;
}

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

    @JsonMember// Language
    language: Language;
}

Und das ist es, wir haben zur Laufzeit die erforderlichen Typinformationen, die jetzt verarbeitet werden können.

Verarbeitung von Typinformationen

Wir müssen zuerst eine Object-Instanz mit JSON.parse erhalten - danach können wir die Elemente in __propertyTypes__ (oben gesammelt) durchlaufen und die erforderlichen Eigenschaften entsprechend instanziieren. Der Typ des Stammobjekts muss angegeben werden, damit der Deserialisierer einen Startpunkt hat.

Eine einfache Umsetzung dieses Ansatzes wäre wiederum:

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

Die obige Idee hat einen großen Vorteil der Deserialisierung durch erwartet Types (für komplexe/Objektwerte) anstelle der in der JSON vorhandenen. Wenn eine Person erwartet wird, wird eine Person-Instanz erstellt. Mit einigen zusätzlichen Sicherheitsmaßnahmen für primitive Typen und Arrays kann dieser Ansatz sicher gemacht werden, sodass any böswilliger JSON widerstehen kann.

Edge-Fälle

Wenn Sie nun jedoch glücklich sind, dass die Lösung das einfach ist, habe ich einige schlechte Neuigkeiten: Es gibt eine (riesige Anzahl von Edge-Fällen, die erledigt werden müssen. Nur einige davon sind:

  • Arrays und Array-Elemente (insbesondere in verschachtelten Arrays)
  • Polymorphismus
  • Abstrakte Klassen und Schnittstellen
  • ...

Wenn Sie nicht mit all dem herumspielen wollen (ich wette, Sie tun es nicht), würde ich mich freuen, eine funktionierende experimentelle Version eines Proof-of-Concept zu empfehlen, die diesen Ansatz verwendet, TypedJSON - - das ich geschaffen habe, um genau dieses Problem anzugehen, ein Problem, dem ich mich täglich gegenüberstelle.

Da Dekorateure immer noch als experimentell betrachtet werden, würde ich die Verwendung für die Produktion nicht empfehlen, aber bisher hat es mir gut getan.

29
John Weisz

sie können Object.assign verwenden. Ich weiß nicht, wann dies hinzugefügt wurde. Ich verwende derzeit TypeScript 2.0.2 und dies scheint eine ES6-Funktion zu sein.

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

hier ist HalJson

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

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

export interface Link {
    readonly href: URL;
}

hier ist was Chrom sagt, dass es ist

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

sie können also sehen, dass die Zuweisung nicht rekursiv erfolgt

26
xenoterracide

Ich habe diesen Typen benutzt, um die Arbeit zu erledigen: https://github.com/weichx/cerialize

Es ist sehr einfach und doch mächtig. Es unterstützt:

  • Serialisierung und Deserialisierung eines ganzen Objektbaums.
  • Persistente und vorübergehende Eigenschaften für dasselbe Objekt.
  • Hooks zum Anpassen der (De) Serialisierungslogik.
  • Es kann in eine vorhandene Instanz (de) serialisiert (ideal für Angular) oder neue Instanzen generiert werden.
  • usw.

Beispiel:

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é

Option 5: TypeScript-Konstruktoren und jQuery.extend verwenden

Dies scheint die am besten wartbare Methode zu sein: Fügen Sie einen Konstruktor hinzu, der die Json-Struktur als Parameter übernimmt, und erweitern Sie das Json-Objekt. Auf diese Weise können Sie eine Json-Struktur in das gesamte Anwendungsmodell integrieren.

Es ist nicht erforderlich, Schnittstellen oder Eigenschaften in Konstruktor zu erstellen.

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 Ihrem Ajax-Callback erhalten Sie eine Firma zur Berechnung der Gehälter:

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

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

Ich habe ein Tool erstellt, das TypeScript-Schnittstellen und eine Laufzeit-Typzuordnung zum Durchführen der Typüberprüfung der Laufzeit anhand der Ergebnisse von JSON.parse: ts.quicktype.io erstellt

Zum Beispiel, gegebene JSON:

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

quicktype erzeugt die folgende TypeScript-Schnittstelle und Typzuordnung:

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

Dann überprüfen wir das Ergebnis von JSON.parse anhand der Typenkarte:

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

Ich habe etwas Code ausgelassen, aber Sie können versuchen, quicktype für die Details.

3
David Siegel

Für einfache Objekte mag ich diese Methode:

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

Durch die Möglichkeit, Eigenschaften im Konstruktor zu definieren, lässt sich dieser kurz fassen.

Dadurch erhalten Sie ein typisiertes Objekt (im Vergleich zu allen Antworten, die Object.assign oder eine Variante verwenden, die ein Objekt enthält) und erfordert keine externen Bibliotheken oder Dekorateure.

1
stevex

JQuery .extend erledigt dies für Sie:

var mytsobject = new mytsobject();

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

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

Die oben beschriebene vierte Option ist eine einfache und schöne Möglichkeit, dies zu tun, die mit der zweiten Option kombiniert werden muss, wenn Sie eine Klassenhierarchie wie beispielsweise eine Mitgliederliste behandeln müssen, die Vorkommnisse von Unterklassen von eine Member-Superklasse, z. B. Director erweitert Member oder Student erweitert Member. In diesem Fall müssen Sie den Unterklassentyp im json-Format angeben

1
Xavier Méhaut

Eine weitere Option mit Fabriken

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

verwenden Sie so

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. hält den Unterricht einfach
  2. injektion für die Fabriken für Flexibilität
0

das Beste, was ich für diesen Zweck gefunden habe, ist der Class-Transformator. github.com/typestack/class-transformer

So benutzen Sie es: 

Einige klasse:

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

Wenn Sie den @Type-Dekorator verwenden, werden auch verschachtelte Eigenschaften erstellt. 

0
Fabianus

Vielleicht keine wirkliche, aber einfache Lösung:

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

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

arbeitet auch für schwierige Abhängigkeiten !!!