如何用JSON对象初始化一个打字稿对象

我从AJAX调用收到一个JSON对象到REST服务器。 这个对象有属性名称匹配我的打字稿类(这是这个问题的后续)。

什么是最好的方式来初始化它? 我不认为这将工作,因为类(&JSON对象)的成员是对象和成员是类的列表,这些类的成员是列表和/或类。

但我更喜欢一种查找成员名称并将它们分配给对象的方法,根据需要创build列表和实例化类,所以我不必为每个类中的每个成员编写明确的代码(有很多!

这些是一些快速镜头,以显示几种不同的方式。 他们绝不是“完整的”,作为一个声明,我不认为这样做是个好主意。 而且代码也不是很干净,因为我只是很快地把它们打在一起。

另外值得一提的是:当然反序列化的类需要有默认的构造函数,就像其他所有我知道任何types的反序列化的语言一样。 当然,如果你调用一个没有参数的非默认构造函数,Javascript就不会抱怨,但是这个类更好地为它做好准备(再加上它不会是“typescripty方式”)。

选项#1:根本没有运行时信息

这种方法的问题主要是任何成员的名字必须与其类相匹配。 这会自动将您限制为每个class级中同一types的成员,并打破好的练习的几条规则。 我强烈build议不要这样做,只是在这里列出来,因为这是我写这个答案时的第一个“草案”(这也是为什么名称是“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: 名称属性

为了摆脱选项#1中的问题,我们需要知道JSON对象中节点的types。 问题是,在Typescript中,这些东西是编译时的结构,我们在运行时需要它们,但运行时对象在设置之前根本不知道它们的属性。

一种办法是让class级知道他们的名字。 不过,您也需要在JSON中使用此属性。 其实,你需要在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); 

选项#3:明确指出成员types

如上所述,类成员的types信息在运行时不可用 – 除非我们使其可用。 我们只需要为非原始成员这样做,我们很好去:

 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:详细,但整洁的方式

更新01/03/2016:正如@GameAlchemist在评论中指出的那样,对于Typescript 1.7,下面描述的解决scheme可以使用类/属性修饰器以更好的方式编写。

序列化总是一个问题,在我看来,最好的办法就是不是最短的。 在所有选项中,这是我所喜欢的,因为类的作者完全控制了反序列化对象的状态。 如果我不得不猜测,我会说,所有其他的select迟早会让你陷入困境(除非Javascript用本地方式处理这个问题)。

真的,下面的例子没有做到灵活性正义。 它确实只是复制类的结构。 但是,你必须记住的区别在于,类可以完全控制使用任何types的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); 

TLDR: TypedJSON (概念的工作certificate)


这个问题的复杂性的根源在于,我们需要在运行时使用仅在编译时存在的types信息来反序列化JSON。 这要求types信息在运行时以某种方式提供。

幸运的是,这可以用装饰器和ReflectDecorators以非常优雅和可靠的方式解决 :

  1. 在属性序列化的属性上使用属性装饰器 ,logging元数据信息并将信息存储在某个地方,例如在类的原型上
  2. 将此元数据信息提供给recursion初始化程序(反序列化程序)

loggingtypes信息

使用ReflectDecorators和属性装饰器的组合,types信息可以很容易地logging有关属性。 这种方法的基本实施将是:

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

就是这样,我们在运行时具有所需的types信息,现在可以处理这些信息。

处理types – 信息

我们首先需要使用JSON.parse来获得一个Object实例 – 之后,我们可以迭代__propertyTypes __propertyTypes__ (上面收集的)中的__propertyTypes__并相应地实例化所需的属性。 必须指定根对象的types,以便解串器具有起点。

再一次,这个方法的简单实现就是:

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

上述想法有一个很大的优点,就是预期的types(对于复杂/对象值)的反序列化,而不是JSON中的内容。 如果期望一个Person ,那么它是一个Person实例被创build。 通过对原始types和数组使用一些额外的安全措施,可以使这种方法变得安全,抵御任何恶意的JSON。

边缘情况

不过,如果你现在很高兴这个解决scheme很简单,那么我有一个坏消息:有大量的边缘案例需要照顾。 其中只有一些是:

  • 数组和数组元素(特别是在嵌套数组中)
  • 多态性
  • 抽象类和接口

如果你不想摆弄所有这些(我打赌你没有),我会很高兴推荐一个工作实验版本的概念validation利用这种方法, TypedJSON – 我创build解决这个确切的问题,我每天面对的问题。

由于如何装修仍被视为实验,我不会推荐使用它的生产使用,但到目前为止,它给了我很好的。

你可以使用Object.assign我不知道这是什么时候添加,我目前正在使用Typescript 2.0.2,这似乎是一个ES6function。

 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 

所以你可以看到它不会recursion地分配

我一直在使用这个人来做这个工作: https : //github.com/weichx/cerialize

这是非常简单而强大的。 它支持:

  • 对象的整个树的序列化和反序列化。
  • 持久性和瞬态属性在同一个对象上。
  • 挂钩来定制(de)序列化逻辑。
  • 它可以(去)序列化到一个现有的实例(伟大的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); 

上面描述的第四个选项是一个简单而又好用的方法,它必须和第二个选项相结合,在这种情况下,你必须处理一个类的层次结构,例如成员列表,这个成员列表是任何一个子类一个会员超级class,例如主任扩展会员或学生扩展会员。 在这种情况下,你必须给出json格式的子types

也许不是实际的,但简单的解决scheme

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

为困难的依赖工作!

选项#5:使用Typescript构造函数和jQuery.extend

这似乎是最可维护的方法:添加一个构造函数作为参数的JSON结构,并扩展JSON对象。 这样你可以把json结构parsing成整个应用程序模型。

没有必要创build接口,或者在构造函数中列出属性。

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

在你的ajaxcallback你收到一家公司来计算薪水:

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

JQuery.extend为你做这个:

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

我创build了一个工具来生成TypeScript接口和一个运行时“types映射”,用于对JSON.parse的结果执行运行时types检查: ts.quicktype.io

例如,给这个JSON:

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

quicktype生成以下TypeScript接口并inputmap:

 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的结果是否符合types映射:

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

我遗漏了一些代码,但你可以尝试quicktype的细节。

另一种select使用工厂

 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 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. 保持你的课程简单
  2. 注入可用于工厂的灵活性

你可以像下面这样做

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

 var instance: Instance = <Instance>({ id: null, name: '', type: '' });