如何使用JSON对象初始化TypeScript对象

Dav*_*len 179 json typescript

我从AJAX调用到REST服务器收到一个JSON对象.此对象具有与我的TypeScript类匹配的属性名称(这是此问题的后续内容).

初始化它的最佳方法是什么?我认为不会起作用,因为类(&JSON对象)的成员是对象列表和成员类,而这些类的成员是列表和/或类.

但我更喜欢一种方法,它查找成员名称并将它们分配,创建列表并根据需要实例化类,因此我不必为每个类中的每个成员编写显式代码(有很多!)

Ing*_*ürk 181

这些是一些快速拍摄,以显示几种不同的方式.它们绝不是"完整的",作为免责声明,我不认为这样做是个好主意.此外,代码不是太干净,因为我只是很快地将它们拼凑在一起.

另外作为注释:当然,可解串的类需要有默认的构造函数,就像我知道任何类型的反序列化的所有其他语言一样.当然,如果你调用一个没有参数的非默认构造函数,Javascript就不会抱怨,但是这个类更好地为它做好准备(而且,它实际上不是"typescripty方式").

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

这种方法的问题主要是任何成员的名称必须与其类匹配.这会自动将您限制为每个班级相同类型的一名成员,并违反了一些良好做法规则.我强烈反对这一点,但只是在这里列出,因为这是我写这个答案时的第一个"草稿"(这也是为什么名字是"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);
Run Code Online (Sandbox Code Playgroud)

选项#2:名称属性

为了摆脱选项#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);
Run Code Online (Sandbox Code Playgroud)

选项#3:明确说明成员类型

如上所述,类成员的类型信息在运行时不可用 - 除非我们使其可用.我们只需要为非原始成员这样做,我们很高兴:

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);
Run Code Online (Sandbox Code Playgroud)

选项#4:冗长而又整洁的方式

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);
Run Code Online (Sandbox Code Playgroud)

  • 选项#4是我称之为合理的方式.您仍然需要编写反序列化代码,但它在同一个类中并且完全可控.如果你来自Java,那么这就像必须编写`equals`或`toString`方法(只有你通常自动生成它们).如果你愿意的话,为'deserialize`编写一个生成器也不应该太难*,但它不能是运行时自动化. (11认同)
  • @IngoBürk,我知道2年后我会问这个问题,但这对于一系列物体有什么作用呢?上面的示例代码适用于JSON对象.它如何用于对象数组? (2认同)
  • 旁注:自1.7以来(不可否认比你的答案更新),typescript提供类/属性装饰器,允许以更整洁的方式编写第4个解决方案. (2认同)
  • 我找到的最好的文档是 StackOverflow 答案:http://stackoverflow.com/a/29837695/856501。我在我的一个项目中使用了装饰器,虽然我想要一些其他功能,但我不得不说它们非常有用。 (2认同)
  • 我现在还不会为一个生产项目使用装饰器-请记住,它们仍然是实验功能。我不会在“实验”上建立真实的代码,因为就我们而言,它们可能会在下一个版本中消失,而您将不得不重写一堆代码,或者永远停留在旧的TS版本上。就是我的$ .02 (2认同)

Joh*_*isz 33

TLDR:TypedJSON(概念的工作证明)


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

幸运的是,这可以通过装饰器ReflectDecorators以非常优雅和强大的方式解决:

  1. 在属于序列化的属性上使用属性修饰符,以记录元数据信息并在某处存储该信息,例如在类原型上
  2. 将此元数据信息提供给递归初始值设定项(解串器)

 

记录类型信息

通过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);
}
Run Code Online (Sandbox Code Playgroud)

对于任何给定的属性,上面的代码片段会将属性的构造函数的引用添加到__propertyTypes__类原型的hidden 属性中.例如:

class Language {
    @JsonMember // String
    name: string;

    @JsonMember// Number
    level: number;
}

class Person {
    @JsonMember // String
    name: string;

    @JsonMember// Language
    language: Language;
}
Run Code Online (Sandbox Code Playgroud)

就是这样,我们在运行时拥有所需的类型信息,现在可以对其进行处理.

 

处理类型信息

我们首先需要Object使用JSON.parse- 获得一个实例- 之后,我们可以遍历__propertyTypes__(上面收集的)entires 并相应地实例化所需的属性.必须指定根对象的类型,以便反序列化器具有起始点.

同样,这种方法的简单实现将是:

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;
}
Run Code Online (Sandbox Code Playgroud)
var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }';
var person: Person = deserialize(JSON.parse(json), Person);
Run Code Online (Sandbox Code Playgroud)

上述想法具有通过预期类型(对于复杂/对象值)而不是JSON中存在的类型进行反序列化的巨大优势.如果Person是a,那么它是一个Person创建的实例.通过对原始类型和数组采取一些额外的安全措施,可以使这种方法安全,抵御任何恶意JSON.

 

边缘案例

但是,如果你现在高兴的是,解决方案简单的,我有一些坏消息:有一个广阔的需要被照顾的边缘情况数.其中只有一些是:

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

如果你不想摆弄所有这些(我打赌你不这样做),我很乐意推荐使用这种方法的TypedJSON概念验证的工作实验版本- 我创造了解决这个确切的问题,这是我每天面对的一个问题.

由于装饰器仍然被认为是实验性的,我不建议将它用于生产用途,但到目前为止它对我很有帮助.


xen*_*ide 33

你可以使用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 );
Run Code Online (Sandbox Code Playgroud)

这里的 HalJson

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

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

export interface Link {
    readonly href: URL;
}
Run Code Online (Sandbox Code Playgroud)

这就是铬所说的

HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public
Run Code Online (Sandbox Code Playgroud)

所以你可以看到它没有递归地分配

  • @Blauhim因为`Object.assign`不会递归地工作,并且不实例化正确的对象类型,所以将值保留为`Object`实例.虽然它对于琐碎的任务来说很好,但它不可能实现复杂的类型序列化.例如,如果类属性是自定义类类型,则`JSON.parse` +`Object.assign`将该属性实例化为`Object`.副作用包括缺少方法和访问者. (15认同)
  • 所以,基本上就是这样:`Object.assign`.为什么我们在这个之上有两个类似词典的答案呢? (2认同)

And*_*dré 12

我一直在用这个人做这个工作: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);
Run Code Online (Sandbox Code Playgroud)


Kim*_*Man 7

这是我的方法(非常简单):

const jsonObj: { [key: string]: any } = JSON.parse(jsonStr);

for (const key in jsonObj) {
  if (!jsonObj.hasOwnProperty(key)) {
    continue;
  }

  console.log(key); // Key
  console.log(jsonObj[key]); // Value
  // Your logic...
}
Run Code Online (Sandbox Code Playgroud)


Dav*_*gel 5

我创建了产生打字稿接口和一个运行时“型图”进行对抗的结果运行时类型检查的工具JSON.parsets.quicktype.io

例如,给定此JSON:

{
  "name": "David",
  "pets": [
    {
      "name": "Smoochie",
      "species": "rhino"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

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",
    },
};
Run Code Online (Sandbox Code Playgroud)

然后我们JSON.parse对照类型图检查结果:

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

我省略了一些代码,但是您可以尝试使用quicktype以获得详细信息。


ste*_*vex 5

对于简单的对象,我喜欢这种方法:

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'});
Run Code Online (Sandbox Code Playgroud)

利用在构造函数中定义属性的能力让它变得简洁。

这将为您提供一个类型化的对象(与使用 Object.assign 或某些变体的所有答案相比,这些答案为您提供一个对象)并且不需要外部库或装饰器。


归档时间:

查看次数:

197200 次

最近记录:

5 年,9 月 前