web-dev-qa-db-fra.com

Comment initialiser un objet TypeScript avec un objet JSON

Je reçois un objet JSON d'un appel AJAX à un serveur REST. Cet objet a des noms de propriété qui correspondent à ma classe TypeScript (il s'agit d'une suite de cette question ).

Quelle est la meilleure façon de l'initialiser? Je ne pense pas que ceci fonctionnera parce que la classe (& objet JSON) a des membres qui sont des listes d'objets et des membres qui sont des classes et que ces classes ont des membres qui sont des listes et/ou des classes.

Mais je préférerais une approche qui recherche les noms des membres et les assigne, en créant des listes et en instanciant les classes selon les besoins. Je n'ai donc pas à écrire de code explicite pour chaque membre de chaque classe (il y en a beaucoup!)

155
David Thielen

Voici quelques exemples rapides illustrant différentes manières. Ils ne sont en aucun cas "complets" et à titre de désistement, je ne pense pas que ce soit une bonne idée de le faire comme ça. De plus, le code n'est pas trop propre car je l'ai simplement tapé assez rapidement.

Remarque: bien sûr, les classes désérialisables doivent avoir des constructeurs par défaut, comme c'est le cas dans toutes les autres langues où je suis conscient de la désérialisation de toutes sortes. Bien sûr, Javascript ne se plaindra pas si vous appelez un constructeur autre que celui par défaut sans arguments, mais il est préférable que la classe soit préparée à cela alors (en plus, ce ne serait pas vraiment la "méthode typographique").

Option n ° 1: aucune information d'exécution du tout

Le problème avec cette approche est principalement que le nom d’un membre doit correspondre à sa classe. Ce qui vous limite automatiquement à un membre du même type par classe et enfreint plusieurs règles de bonne pratique. Je vous le déconseille vivement, mais indiquez-le ici car il s’agissait du premier "projet" lorsque j’ai écrit cette réponse (c’est aussi pourquoi les noms sont "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 n ° 2: Le nom propriété

Pour éliminer le problème de l'option n ° 1, nous devons disposer d'informations de type type de nœud dans l'objet JSON. Le problème est que, dans TypeScript, ces éléments sont des constructions au moment de la compilation et nous en avons besoin au moment de l'exécution - mais les objets d'exécution n'ont simplement aucune connaissance de leurs propriétés tant qu'ils ne sont pas définis.

Une façon de le faire est de renseigner les classes sur leurs noms. Cependant, vous avez également besoin de cette propriété dans le JSON. En fait, vous seulement en avez besoin dans le 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 n ° 3: énoncer explicitement les types de membre

Comme indiqué ci-dessus, les informations de type des membres de classe ne sont pas disponibles au moment de l'exécution, à moins que nous ne les rendions disponibles. Nous ne devons le faire que pour les membres non-primitifs et nous sommes prêts à partir:

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 n ° 4: la manière verbeuse, mais soignée

Mise à jour 01/03/2016: Comme @GameAlchemist l'a souligné dans les commentaires, à partir de TypeScript 1.7, la solution décrite ci-dessous peut être mieux écrite à l'aide de décorateurs de classe/propriété.

La sérialisation est toujours un problème et à mon avis, le meilleur moyen est tout simplement le plus court. C'est ce que je préférerais parmi toutes les options, car l'auteur de la classe a le contrôle total sur l'état des objets désérialisés. Si je devais deviner, je dirais que toutes les autres options, tôt ou tard, vous poseront des problèmes (à moins que Javascript ne propose un moyen natif de gérer cela).

En réalité, l'exemple suivant ne rend pas justice à la flexibilité. Cela ne fait que copier la structure de la classe. La différence que vous devez garder à l'esprit ici, cependant, est que la classe a le plein contrôle pour utiliser n'importe quel type de JSON pour contrôler l'état de la classe entière (vous pouvez calculer des choses, 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);
162
Ingo Bürk

vous pouvez utiliser Object.assign Je ne sais pas quand cela a été ajouté, j'utilise actuellement TypeScript 2.0.2 et il semble que ce soit une fonctionnalité ES6.

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

voici HalJson

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

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

export interface Link {
    readonly href: URL;
}

voici ce que chrome dit qu'il est

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

de sorte que vous pouvez voir qu'il ne fait pas l'assignation récursivement

26
xenoterracide

J'utilise ce type pour faire le travail: https://github.com/weichx/cerialize

C'est très simple mais puissant. Elle supporte:

  • Sérialisation et désérialisation de tout un arbre d'objets.
  • Propriétés persistantes et transitoires sur le même objet.
  • Crochets pour personnaliser la logique de (dé) sérialisation.
  • Il peut (dés) sérialiser dans une instance existante (idéal pour Angular) ou générer de nouvelles instances.
  • etc.

Exemple:

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 n ° 5: Utiliser les constructeurs TypeScript et jQuery.extend

Cela semble être la méthode la plus facile à gérer: ajoutez un constructeur qui prend en paramètre la structure json et étendez l'objet json. De cette façon, vous pouvez analyser une structure JSON dans l'ensemble du modèle d'application.

Il n'est pas nécessaire de créer des interfaces ou de lister les propriétés dans le constructeur.

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

Dans votre rappel ajax où vous recevez une entreprise pour calculer les salaires:

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

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

J'ai créé un outil qui génère des interfaces TypeScript et une "mappe de types" d'exécution permettant d'effectuer une vérification typographique à l'exécution avec les résultats de JSON.parse: ts.quicktype.io

Par exemple, étant donné ce JSON:

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

quicktype produit l'interface TypeScript et le type map suivants:

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

Ensuite, nous comparons le résultat de JSON.parse avec le type map:

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

J'ai laissé du code, mais vous pouvez essayer quicktype pour les détails.

3
David Siegel

Pour les objets simples, j'aime cette méthode:

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

Exploiter la possibilité de définir des propriétés dans le constructeur permet d’être concis.

Cela vous donne un objet typé (contre toutes les réponses qui utilisent Object.assign ou une variante qui vous donne un objet) et ne nécessite pas de bibliothèques externes ni de décorateurs.

1
stevex

JQuery .extend le fait pour vous:

var mytsobject = new mytsobject();

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

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

La 4ème option décrite ci-dessus est une façon simple et agréable de le faire, qui doit être combinée avec la 2ème option dans le cas où vous devez gérer une hiérarchie de classes, comme par exemple une liste de membres qui est l'une des occurrences de sous-classes de une super-classe de membres, par exemple directeur élargissant membre ou étudiant élargissant membre. Dans ce cas, vous devez donner le type de sous-classe au format json

1
Xavier Méhaut

Une autre option utilisant des usines

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

utiliser comme ça

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. garde vos cours simples
  2. injection disponible pour les usines pour plus de flexibilité
0
Anthony Johnston

le meilleur que j'ai trouvé à cet effet est le transformateur de classe. github.com/typestack/class-transformer

C'est comme ça que vous l'utilisez: 

Un peu de classe:

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

Si vous utilisez le décorateur @Type, des propriétés imbriquées seront également créées. 

0
Fabianus

Peut-être pas réelle, mais solution simple:

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

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

travaillez aussi pour les dépendances difficiles !!!