我从AJAX调用REST服务器收到一个JSON对象。该对象具有与我的TypeScript类匹配的属性名称(这是此问题的后续内容)。
初始化它的最佳方法是什么?我认为这行不通,因为类(&JSON对象)具有作为对象列表的成员和作为类的成员,而这些类具有作为列表和/或类的成员。
但是我更喜欢一种查找成员名称并在其间分配它们,创建列表并根据需要实例化类的方法,因此我不必为每个类中的每个成员编写明确的代码(有很多!)
我从AJAX调用REST服务器收到一个JSON对象。该对象具有与我的TypeScript类匹配的属性名称(这是此问题的后续内容)。
初始化它的最佳方法是什么?我认为这行不通,因为类(&JSON对象)具有作为对象列表的成员和作为类的成员,而这些类具有作为列表和/或类的成员。
但是我更喜欢一种查找成员名称并在其间分配它们,创建列表并根据需要实例化类的方法,因此我不必为每个类中的每个成员编写明确的代码(有很多!)
Answers:
这些是一些快速快照,它们显示了几种不同的方式。它们绝不是“完整的”,作为免责声明,我认为这样做不是一个好主意。代码也不是很干净,因为我只是很快地将它们键入在一起。
另请注意:可反序列化的类当然需要具有默认的构造函数,就像我知道任何反序列化的所有其他语言一样。当然,如果您调用不带参数的非默认构造函数,那么Javascript不会抱怨,但是该类最好为此做准备(此外,它实际上不是“打字方法”)。
这种方法的问题主要是任何成员的名称都必须与其类匹配。这会自动将您限制为每个班级一名相同类型的成员,并破坏了一些良好实践的规则。我强烈建议您这样做,但请在此处列出,因为这是我编写此答案时的第一个“草稿”(这也是为什么名称为“ 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);
为了摆脱选项1中的问题,我们需要掌握JSON对象中节点的类型的某种信息。问题在于,在Typescript中,这些都是编译时构造,我们在运行时需要它们-但是运行时对象只是在设置它们之前才意识到它们的属性。
一种方法是让类知道其名称。不过,您也需要在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);
如上所述,类成员的类型信息在运行时不可用–除非我们使它可用。我们只需要对非原始成员执行此操作,我们很好:
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);
2016年1月3日更新:正如@GameAlchemist在评论中指出的(想法,实现)中 Typescript 1.7开始,可以使用类/属性装饰器以更好的方式编写以下描述的解决方案。
序列化始终是一个问题,我认为最好的方法就是最短的方法。在所有选项中,这是我想要的,因为该类的作者完全控制了反序列化对象的状态。如果我不得不猜测,我会说所有其他选择迟早都会给您带来麻烦(除非Javascript提出了一种本机处理方式)。
的确,以下示例并未体现灵活性。它确实确实只是复制了类的结构。不过,您必须在这里记住的区别是,该类具有完全控制权,可以使用它想要控制整个类的状态的任何类型的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);
equals
或toString
方法(仅通常使它们自动生成)。如果您愿意的话,为它编写一个生成器应该不会太难deserialize
,但是它不能是运行时自动化。
您可以使用Object.assign
我不知道何时添加它,我当前正在使用Typescript 2.0.2,这似乎是ES6功能。
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
所以你可以看到它没有递归分配
Object.assign
。那么为什么在这个答案之上有两个类似词典的答案呢?
Object.assign
将无法递归工作,并且不会实例化正确的对象类型,而将值保留为Object
实例。尽管可以完成琐碎的任务,但是复杂类型的序列化是不可能的。例如,如果类属性为自定义类类型,则JSON.parse
+ Object.assign
会将其实例化为Object
。副作用包括缺少方法和访问器。
Object.assign
,它仍然归结为通过嵌套嵌套实例化手。这种方法适用于非常简单的教程级对象,但不适用于实际用途。
TLDR:TypedJSON(有效的概念证明)
这个问题复杂性的根源在于,我们需要在运行时使用仅在编译时存在的类型信息对JSON进行反序列化。这要求在运行时以某种方式提供类型信息。
幸运的是,可以使用装饰器和ReflectDecorators以非常优雅和健壮的方式解决此问题:
结合使用ReflectDecorators和属性装饰器,可以轻松记录有关属性的类型信息。此方法的基本实现为:
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__
类原型的hidden 属性上。例如:
class Language {
@JsonMember // String
name: string;
@JsonMember// Number
level: number;
}
class Person {
@JsonMember // String
name: string;
@JsonMember// Language
language: Language;
}
就是这样,我们在运行时拥有必需的类型信息,现在可以对其进行处理。
我们首先需要Object
使用JSON.parse
- 获得一个实例,然后,我们可以遍历__propertyTypes__
(收集到的)整个实例,并相应地实例化所需的属性。必须指定根对象的类型,以便解串器具有起点。
再次,这种方法的简单实现将是:
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);
上面的想法有一个很大的优势,那就是按期望的类型(对于复杂/对象值)反序列化,而不是JSON中的反序列化。如果Person
期望a,那么将Person
创建一个实例。通过针对原始类型和数组采取一些其他安全措施,可以使此方法变得安全,可以抵御任何恶意JSON。
但是,如果你现在高兴的是,解决方案是简单的,我有一些坏消息:有一个广阔的需要被照顾的边缘情况数。仅其中一些是:
如果您不想摆弄所有这些内容(我敢打赌您不愿意),我很高兴为您推荐一个使用这种方法的实验性概念验证版本,即TypedJSON,这是我创建的为了解决这个确切的问题,我每天都会面对自己的问题。
由于装饰器仍处于实验状态,因此我不建议将其用于生产用途,但到目前为止,它对我来说还是不错的。
我一直在用这个人来做这项工作:https : //github.com/weichx/cerialize
它非常简单但功能强大。它支持:
例:
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);
我创建了产生打字稿接口和一个运行时“型图”进行对抗的结果运行时类型检查的工具JSON.parse
:ts.quicktype.io
例如,给定此JSON:
{
"name": "David",
"pets": [
{
"name": "Smoochie",
"species": "rhino"
}
]
}
quicktype产生以下TypeScript接口和类型映射:
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
对照类型图检查结果:
export function fromJson(json: string): Person {
return cast(JSON.parse(json), object("Person"));
}
我遗漏了一些代码,但是您可以尝试使用quicktype获取详细信息。
这似乎是最可维护的方法:添加一个将json结构作为参数的构造函数,并扩展json对象。这样,您可以将json结构解析为整个应用程序模型。
无需创建接口或在构造函数中列出属性。
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;
}
}
在您接收到公司来计算工资的ajax回调中:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
newCompany.calculateSalaries()
}
$.extend
来的?
Object.assign
,这将删除对jQuery的依赖。
对于简单的对象,我喜欢这种方法:
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'});
利用在构造函数中定义属性的能力,可以使其简洁。
这将为您提供一个类型化的对象(与使用Object.assign或某些变体的所有答案(为您提供一个对象)相对),并且不需要外部库或装饰器。
上面描述的第4个选项是一种简单而不错的方法,在必须处理类层次结构(例如,成员列表是任何子类的出现)的情况下,必须与第二个选项结合使用成员超类,例如,主管扩展成员或学生扩展成员。在这种情况下,您必须以json格式提供子类类型
也许不是实际的,但是简单的解决方案:
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
也为困难的依赖而工作!!!
baz
则将是类型,Object
而不是类型Bar.
。在这种简单情况下,因为Bar
没有方法(仅是基本属性),它才有效。如果Bar
使用类似的方法isEnabled()
,则该方法将失败,因为该方法将不在序列化的JSON字符串中。
使用工厂的另一种选择
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 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);
我为此找到的最好的是class-transformer。github.com/typestack/class-transformer
那就是你的用法:
某类:
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);
}
如果使用@Type装饰器,则也会创建嵌套属性。
我个人更喜欢@IngoBürk的选项#3。而且我改进了他的代码,以支持复杂数据数组和原始数据数组。
interface IDeserializable {
getTypes(): Object;
}
class Utility {
static deserializeJson<T>(jsonObj: object, classType: any): T {
let instanceObj = new classType();
let types: IDeserializable;
if (instanceObj && instanceObj.getTypes) {
types = instanceObj.getTypes();
}
for (var prop in jsonObj) {
if (!(prop in instanceObj)) {
continue;
}
let jsonProp = jsonObj[prop];
if (this.isObject(jsonProp)) {
instanceObj[prop] =
types && types[prop]
? this.deserializeJson(jsonProp, types[prop])
: jsonProp;
} else if (this.isArray(jsonProp)) {
instanceObj[prop] = [];
for (let index = 0; index < jsonProp.length; index++) {
const elem = jsonProp[index];
if (this.isObject(elem) && types && types[prop]) {
instanceObj[prop].push(this.deserializeJson(elem, types[prop]));
} else {
instanceObj[prop].push(elem);
}
}
} else {
instanceObj[prop] = jsonProp;
}
}
return instanceObj;
}
//#region ### get types ###
/**
* check type of value be string
* @param {*} value
*/
static isString(value: any) {
return typeof value === "string" || value instanceof String;
}
/**
* check type of value be array
* @param {*} value
*/
static isNumber(value: any) {
return typeof value === "number" && isFinite(value);
}
/**
* check type of value be array
* @param {*} value
*/
static isArray(value: any) {
return value && typeof value === "object" && value.constructor === Array;
}
/**
* check type of value be object
* @param {*} value
*/
static isObject(value: any) {
return value && typeof value === "object" && value.constructor === Object;
}
/**
* check type of value be boolean
* @param {*} value
*/
static isBoolean(value: any) {
return typeof value === "boolean";
}
//#endregion
}
// #region ### Models ###
class Hotel implements IDeserializable {
id: number = 0;
name: string = "";
address: string = "";
city: City = new City(); // complex data
roomTypes: Array<RoomType> = []; // array of complex data
facilities: Array<string> = []; // array of primitive data
// getter example
get nameAndAddress() {
return `${this.name} ${this.address}`;
}
// function example
checkRoom() {
return true;
}
// this function will be use for getting run-time type information
getTypes() {
return {
city: City,
roomTypes: RoomType
};
}
}
class RoomType implements IDeserializable {
id: number = 0;
name: string = "";
roomPrices: Array<RoomPrice> = [];
// getter example
get totalPrice() {
return this.roomPrices.map(x => x.price).reduce((a, b) => a + b, 0);
}
getTypes() {
return {
roomPrices: RoomPrice
};
}
}
class RoomPrice {
price: number = 0;
date: string = "";
}
class City {
id: number = 0;
name: string = "";
}
// #endregion
// #region ### test code ###
var jsonObj = {
id: 1,
name: "hotel1",
address: "address1",
city: {
id: 1,
name: "city1"
},
roomTypes: [
{
id: 1,
name: "single",
roomPrices: [
{
price: 1000,
date: "2020-02-20"
},
{
price: 1500,
date: "2020-02-21"
}
]
},
{
id: 2,
name: "double",
roomPrices: [
{
price: 2000,
date: "2020-02-20"
},
{
price: 2500,
date: "2020-02-21"
}
]
}
],
facilities: ["facility1", "facility2"]
};
var hotelInstance = Utility.deserializeJson<Hotel>(jsonObj, Hotel);
console.log(hotelInstance.city.name);
console.log(hotelInstance.nameAndAddress); // getter
console.log(hotelInstance.checkRoom()); // function
console.log(hotelInstance.roomTypes[0].totalPrice); // getter
// #endregion
你可以像下面这样
export interface Instance {
id?:string;
name?:string;
type:string;
}
和
var instance: 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;
}
}