Comment initialiser un object TypeScript avec un object JSON

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

Quelle est la meilleure façon de l’initialiser? Je ne pense pas que cela fonctionnera car la classe (object JSON) a des membres qui sont des listes d’objects et des membres qui sont des classes, et 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 des classes d’instanciation selon les besoins, donc je n’ai pas besoin d’écrire du code explicite pour chaque membre de chaque classe (il y a beaucoup!)

Ce sont quelques clichés rapides pour montrer différentes manières. Ils ne sont en aucun cas “complets” et en tant que déni, 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 puisque je l’ai simplement tapé ensemble rapidement.

Comme note également: bien sûr, les classes désérialisables doivent avoir des constructeurs par défaut, comme c’est le cas dans tous les autres langages où je suis conscient de la désérialisation de tout type. Bien sûr, Javascript ne va pas se plaindre si vous appelez un constructeur non par défaut sans arguments, mais la classe doit être mieux préparée pour cela (en plus, ce ne serait pas vraiment la “méthode de typage”).

Option n ° 1: Aucune information d’exécution

Le problème avec cette approche est que le nom de tout 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 déconseille fortement cela, mais juste le lister ici parce que c’était le premier “brouillon” quand j’ai écrit cette réponse (ce qui explique 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: la propriété de nom

Pour se débarrasser du problème dans l’option n ° 1, nous devons avoir une sorte d’informations sur le type d’un nœud dans l’object JSON. Le problème est que dans Typescript, ces choses sont des constructions à la compilation et nous en avons besoin au moment de l’exécution, mais les objects d’exécution n’ont simplement pas conscience de leurs propriétés tant qu’elles ne sont pas définies.

Une façon de le faire est de sensibiliser les classes à leurs noms. Vous avez également besoin de cette propriété dans le JSON. En fait, vous n’en avez besoin que dans le 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 n ° 3: Indiquer explicitement les types de membres

Comme indiqué ci-dessus, les informations de type des membres de la classe ne sont pas disponibles au moment de l’exécution, à moins que nous ne les rendions disponibles. Nous avons seulement besoin de le faire pour les membres non primitifs et nous sums heureux d’y aller:

 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 en utilisant des décorateurs de classe / propriété.

La sérialisation est toujours un problème et à mon avis, le meilleur moyen est de ne pas être le plus court. Parmi toutes les options, c’est ce que je préfère car l’auteur de la classe a le contrôle total sur l’état des objects désérialisés. Si je devais deviner, je dirais que toutes les autres options, tôt ou tard, vous causeront des problèmes (à moins que Javascript ne propose une manière native 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 est que la classe a le contrôle total pour utiliser tout type de JSON qu’elle veut contrôler l’état de la classe entière (vous pouvez calculer des choses, etc.).

 interface Serializable { deserialize(input: Object): T; } class Member implements Serializable { id: number; deserialize(input) { this.id = input.id; return this; } } class ExampleClass implements Serializable { 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); 

TLDR: TypedJSON (preuve de concept de travail)


La racine de la complexité de ce problème est que nous devons désérialiser JSON à l’ exécution en utilisant des informations de type qui n’existent qu’au moment de la compilation . Cela nécessite que les informations de type soient en quelque sorte disponibles au moment de l’exécution.

Heureusement, cela peut être résolu de manière très élégante et robuste avec les décorateurs et les ReflectDecorators :

  1. Utiliser des décorateurs de propriétés sur des propriétés soumises à la sérialisation, enregistrer des informations de métadonnées et stocker ces informations quelque part, par exemple sur le prototype de classe
  2. Envoyez ces informations de métadonnées à un initialiseur récursif (désérialiseur)

Informations sur le type d’enregistrement

Grâce à une combinaison de ReflectDecorators et de décorateurs de propriétés, les informations de type peuvent être facilement enregistrées sur une propriété. Une mise en œuvre rudimentaire de cette approche serait:

 function JsonMember(target: any, propertyKey: ssortingng) { 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); } 

Pour toute propriété donnée, l’extrait ci-dessus ajoute une référence de la fonction constructeur de la propriété à la propriété __propertyTypes__ masquée du prototype de classe. Par exemple:

 class Language { @JsonMember // Ssortingng name: ssortingng; @JsonMember// Number level: number; } class Person { @JsonMember // Ssortingng name: ssortingng; @JsonMember// Language language: Language; } 

Et voilà, nous avons les informations de type requirejses à l’exécution, qui peuvent maintenant être traitées.

Type de traitement-Information

Nous devons d’abord obtenir une instance Object utilisant JSON.parse – après cela, nous pouvons effectuer une itération sur les entrées dans __propertyTypes__ (collectées ci-dessus) et instancier les propriétés requirejses en conséquence. Le type de l’object racine doit être spécifié pour que le désérialiseur ait un sharepoint départ.

Encore une fois, une simple implémentation simple de cette approche serait:

 function deserialize(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); 

L’idée ci-dessus a un grand avantage de désérialiser par types attendus (pour les valeurs complexes / objects), au lieu de ce qui est présent dans le JSON. Si une Person est attendue, il s’agit d’une instance de Person créée. Avec certaines mesures de sécurité supplémentaires en place pour les types primitifs et les baies, cette approche peut être sécurisée, ce qui résiste à tout JSON malveillant.

Bordures

Cependant, si vous êtes maintenant heureux que la solution soit aussi simple, j’ai de mauvaises nouvelles: il y a un grand nombre de cas extrêmes à prendre en compte. Seuls certains d’entre eux sont:

  • Tableaux et éléments de tableau (en particulier dans les tableaux nesteds)
  • Polymorphisme
  • Classes abstraites et interfaces

Si vous ne voulez pas vous tromper avec tout ça (je parie que vous ne le faites pas), je serais ravi de vous recommander une version expérimentale de preuve de concept utilisant cette approche, TypedJSON – que j’ai créée pour faire face à ce problème précis, un problème auquel je suis confronté chaque jour.

Étant donné que les décorateurs sont toujours considérés comme expérimentaux, je ne recommanderais pas de l’utiliser pour une utilisation en production, mais jusqu’à présent, cela m’a bien servi.

vous pouvez utiliser Object.assign Je ne sais pas quand cela a été ajouté, j’utilise actuellement Typescript 2.0.2, et cela semble être 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: ssortingng]: Link; } export interface Link { readonly href: URL; } 

voici ce que le 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

J’ai utilisé ce gars pour faire le travail: https://github.com/weichx/cerialize

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

  • Sérialisation et désérialisation d’un arbre entier d’objects.
  • Propriétés persistantes et transitoires sur le même object.
  • Accroche 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 : ssortingng; @deserializeAs(Leaf) public leafs : Array; //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 : ssortingng] : Leaf}; //use an object as a map } class Leaf { @deserialize public color : ssortingng; @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); 

Option n ° 5: Utiliser les constructeurs Typescript et jQuery.extend

Cela semble être la méthode la plus facile à maintenir: ajoutez un constructeur qui prend en paramètre la structure json et étendez l’object json. De cette façon, vous pouvez parsingr une structure json dans le modèle d’application entier.

Il n’est pas nécessaire de créer des interfaces ou de répertorier 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: ssortingng; salary: number; city: ssortingng; 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 callback 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() } 

La 4ème option décrite ci-dessus est une manière 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 parmi les sous-classes de une super classe de membre, par exemple le directeur étend membre ou étudiant étend membre. Dans ce cas, vous devez donner le type de sous-classe au format json

J’ai créé un outil qui génère des interfaces TypeScript et un “type map” d’exécution pour effectuer une vérification de type d’exécution par rapport aux 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 la mappe de type suivantes:

 export interface Person { name: ssortingng; pets: Pet[]; } export interface Pet { name: ssortingng; species: ssortingng; } const typeMap: any = { Person: { name: "ssortingng", pets: array(object("Pet")), }, Pet: { name: "ssortingng", species: "ssortingng", }, }; 

Ensuite, nous vérifions le résultat de JSON.parse rapport à la carte de type:

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

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

Peut-être pas une solution actuelle, mais simple:

 interface Bar{ x:number; y?:ssortingng; } var baz:Bar = JSON.parse(jsonSsortingng); alert(baz.y); 

travailler pour des dépendances difficiles aussi !!!

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 

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 eg dates are Dates console.log('a.date is instanceof Date', a.date instanceof Date); console.log('ab is instanceof B', ab instanceof B); 
  1. garde vos cours simples
  2. injection disponible pour les usines pour la flexibilité

vous pouvez faire comme ci-dessous

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

et

 var instance: Instance = ({ id: null, name: '', type: '' }); 
 **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; } }