Come inizializzare un object TypeScript con un object JSON

Ricevo un object JSON da una chiamata AJAX a un server REST. Questo object ha nomi di proprietà che corrispondono alla mia class TypeScript (questo è un seguito a questa domanda ).

Qual è il modo migliore per inizializzarlo? Non penso che funzionerà perché la class (e l’object JSON) hanno membri che sono liste di oggetti e membri che sono classi, e quelle classi hanno membri che sono liste e / o classi.

Ma preferirei un approccio che guardi i nomi dei membri e li assegni attraverso, creando liste e istanziando le classi secondo necessità, quindi non devo scrivere codice esplicito per ogni membro in ogni class (c’è un sacco!)

Questi sono alcuni scatti veloci a questo per mostrare alcuni modi diversi. Non sono affatto “completi” e in quanto disclaimer, non penso sia una buona idea farlo in questo modo. Inoltre, il codice non è troppo pulito poiché l’ho appena digitato piuttosto rapidamente.

Anche come una nota: Naturalmente le classi deserializable devono avere costruttori predefiniti come è il caso in tutte le altre lingue in cui sono a conoscenza della deserializzazione di qualsiasi tipo. Ovviamente, Javascript non si lamenterà se chiami un costruttore non predefinito senza argomenti, ma la class sarà meglio preparata per questo (inoltre, non sarebbe in realtà il “metodo typescripty”).

Opzione n. 1: nessuna informazione di runtime

Il problema con questo approccio è che il nome di ogni membro deve corrispondere alla sua class. Che limita automaticamente a un membro dello stesso tipo per class e rompe diverse regole di buona pratica. Vi sconsiglio vivamente di farlo, ma elencatelo qui perché è stata la prima “bozza” quando ho scritto questa risposta (che è anche il motivo per cui i nomi sono “Foo” ecc.).

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

Opzione n. 2: la proprietà del nome

Per eliminare il problema nell’opzione n. 1, dobbiamo avere qualche tipo di informazione sul tipo di nodo nell’object JSON. Il problema è che in Typescript queste cose sono costrutti in fase di compilazione e ne abbiamo bisogno al momento dell’esecuzione – ma gli oggetti runtime non hanno consapevolezza delle loro proprietà fino a quando non vengono impostate.

Un modo per farlo è rendere le classi consapevoli dei loro nomi. Hai bisogno di questa proprietà anche nel JSON. In realtà, ne hai solo bisogno in the 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); 

Opzione n. 3: indicare esplicitamente i tipi di membri

Come detto sopra, le informazioni sul tipo dei membri della class non sono disponibili in fase di runtime, cioè a meno che non siano rese disponibili. Abbiamo solo bisogno di farlo per i membri non primitivi e siamo a posto:

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

Opzione 4: il modo verboso, ma pulito

Aggiornamento 01/03/2016: Come @GameAlchemist ha sottolineato nei commenti, a partire da Typescript 1.7, la soluzione descritta di seguito può essere scritta in un modo migliore utilizzando i decoratori di class / proprietà.

La serializzazione è sempre un problema e, secondo me, il modo migliore è un modo che non è il più breve. Tra tutte le opzioni, questo è ciò che preferirei perché l’autore della class ha il pieno controllo sullo stato degli oggetti deserializzati. Se dovessi indovinare, direi che tutte le altre opzioni, prima o poi, ti metteranno nei guai (a meno che Javascript non trovi un modo nativo per occuparsi di questo).

In realtà, il seguente esempio non rende giustizia alla flessibilità. In realtà copia solo la struttura della class. La differenza che devi tenere a mente qui, però, è che la class ha il pieno controllo di usare qualsiasi tipo di JSON che vuole controllare lo stato dell’intera class (puoi calcolare cose, ecc.).

 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 (prova di funzionamento del concetto)


La radice della complessità di questo problema è che abbiamo bisogno di deserializzare JSON in fase di runtime usando informazioni di tipo che esistono solo in fase di compilazione . Ciò richiede che le informazioni sul tipo siano in qualche modo rese disponibili in fase di runtime.

Fortunatamente, questo può essere risolto in modo molto elegante e robusto con decoratori e ReflectDecorators :

  1. Utilizzare i decoratori di proprietà su proprietà soggette a serializzazione, per registrare le informazioni sui metadati e archiviare tali informazioni da qualche parte, ad esempio sul prototipo di class
  2. Invia queste informazioni sui metadati a un inizializzatore ricorsivo (deserializzatore)

Registrazione di informazioni tipo

Con una combinazione di ReflectDecorators e decoratori di proprietà, le informazioni sul tipo possono essere facilmente registrate su una proprietà. Un’implementazione rudimentale di questo approccio sarebbe:

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

Per ogni proprietà data, lo snippet sopra riportato aggiungerà un riferimento della funzione di costruzione della proprietà alla proprietà __propertyTypes__ nascosta sul prototipo di class. Per esempio:

 class Language { @JsonMember // String name: string; @JsonMember// Number level: number; } class Person { @JsonMember // String name: string; @JsonMember// Language language: Language; } 

E questo è tutto, abbiamo le informazioni sul tipo richieste in fase di esecuzione, che ora possono essere elaborate.

Elaborazione informazioni di tipo

Per prima cosa dobbiamo ottenere un’istanza di Object usando JSON.parse – dopo di ciò, possiamo scorrere le entrate in __propertyTypes__ (raccolte sopra) e istanziare di conseguenza le proprietà richieste. Il tipo dell’object root deve essere specificato, in modo che il deserializzatore abbia un punto di partenza.

Ancora una volta, una semplice implementazione di questo approccio sarebbe:

 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’idea di cui sopra ha un grande vantaggio di deserializzare dai tipi previsti (per i valori complessi / object), invece di ciò che è presente nel JSON. Se una Person è prevista, allora è un’istanza Person che viene creata. Con alcune misure di sicurezza aggiuntive in atto per i tipi primitivi e gli array, questo approccio può essere reso sicuro, che resiste a qualsiasi JSON dannoso.

Edge Cases

Tuttavia, se ora sei felice che la soluzione sia così semplice, ho delle brutte notizie: c’è un vasto numero di casi limite che devono essere curati. Solo alcuni dei quali sono:

  • Array ed elementi dell’array (specialmente negli array annidati)
  • Polimorfismo
  • Classi e interfacce astratte

Se non vuoi giocherellare con tutti questi (scommetto che non lo fai), sarei lieto di raccomandare una versione sperimentale funzionante di un proof-of-concept che utilizza questo approccio, TypedJSON – che ho creato per affrontare questo problema esatto, un problema che mi devo affrontare ogni giorno.

A causa di come i decoratori sono ancora considerati sperimentali, non consiglierei di usarlo per l’uso in produzione, ma finora mi è servito bene.

puoi usare Object.assign Non so quando è stato aggiunto, sto usando Typescript 2.0.2, e questa sembra essere una funzione ES6.

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

ecco HalJson

 export class HalJson { _links: HalLinks; } export class HalLinks implements Links { } export interface Links { readonly [text: string]: Link; } export interface Link { readonly href: URL; } 

ecco cosa crede che sia

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

quindi puoi vedere che non lo fa in modo ricorsivo

Ho usato questo ragazzo per fare il lavoro: https://github.com/weichx/cerialize

È molto semplice ma potente. Supporta:

  • Serializzazione e deserializzazione di un intero albero di oggetti.
  • Proprietà persistenti e transitorie sullo stesso object.
  • Hook per personalizzare la logica di (de) serializzazione.
  • Può (de) serializzare in un’istanza esistente (ideale per Angular) o generare nuove istanze.
  • eccetera.

Esempio:

 class Tree { @deserialize public species : string; @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 : 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); 

Opzione 5: utilizzo dei costruttori Typescript e jQuery.extend

Questo sembra essere il metodo più gestibile: aggiungere un costruttore che prende come parametro la struttura JSON ed estendere l’object JSON. In questo modo è ansible analizzare una struttura JSON nell’intero modello di applicazione.

Non è necessario creare interfacce o elencare le proprietà nel costruttore.

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

Nel tuo callback ajax dove ricevi una società per calcolare gli stipendi:

 onReceiveCompany( jsonCompany : any ) { let newCompany = new Company( jsonCompany ); // call the methods on your newCompany object ... newCompany.calculateSalaries() } 

La quarta opzione sopra descritta è un modo semplice e carino per farlo, che deve essere combinato con la seconda opzione nel caso in cui si debba gestire una gerarchia di classi come ad esempio una lista di membri che è una delle occorrenze di sottoclassi di una super class membro, ad esempio il direttore estende il membro o lo studente estende il membro. In tal caso devi fornire il tipo di sottoclass nel formato json

Ho creato uno strumento che genera interfacce TypeScript e una “mappa dei tipi” in runtime per eseguire il typechecking di runtime con i risultati di JSON.parse : ts.quicktype.io

Ad esempio, dato questo JSON:

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

quicktype produce la seguente interfaccia TypeScript e digita 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", }, }; 

Quindi controlliamo il risultato di JSON.parse sulla mappa dei tipi:

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

Ho lasciato un po ‘di codice, ma puoi provare quicktype per i dettagli.

Forse non reale, ma soluzione semplice:

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

lavoro anche per dipendenze difficili !!!

JQuery .extend fa questo per te:

 var mytsobject = new mytsobject(); var newObj = {a:1,b:2}; $.extend(mytsobject, newObj); //mytsobject will now contain a & b 

Un’altra opzione che utilizza le fabbriche

 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

usare così

 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. mantiene le tue lezioni semplici
  2. iniezione disponibile per le fabbriche per la flessibilità

puoi fare come sotto

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

e

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