如何将一个JSON对象转换成TypeScript类?

634

我从远程REST服务器读取了一个JSON对象。这个JSON对象具有TypeScript类的所有属性(出于设计目的)。我该如何将接收到的JSON对象转换为类型变量?

我不想填充TypeScript变量(即具有接受此JSON对象的构造函数)。这很耗时,需要逐个子对象和属性复制。

更新:你可以将其转换为TypeScript接口!请参考此处


1
如果您的JSON是使用Java类映射的,可以使用https://github.com/vojtechhabarta/typescript-generator生成TypeScript接口。 - Vojta
我编写了一个小型的类型转换库:https://sulphur-blog.azurewebsites.net/typescript-mini-cast-library/ - Camille Wintz
11
我已经为此制作了一个工具:https://beshanoe.github.io/json2ts/。 - beshanoe
创建原型 TypeScript 类来定义您的对象不会影响真实的生产代码。查看编译后的 JS 文件,所有定义都将被删除,因为它们不是 JS 的一部分。 - FisNaN
我认为StackOverflow上的任何解决方案都不是该问题的全面解决方案。因此,我创建了一个npm包angular-http-deserializer来解决这个问题:https://www.npmjs.com/package/angular-http-deserializer#usage - Jeff Fischer
这是一个解决方案:https://dev59.com/OVkS5IYBdhLWcg3wASHc#58788876 - egor.xyz
29个回答

222

在Ajax请求中,你不能简单地将一个普通的JavaScript结果转换成一个原型JavaScript/TypeScript类实例。有许多技术可以做到这一点,通常涉及复制数据。除非你创建了该类的实例,否则它将保持为一个简单的JavaScript对象,没有任何方法或属性。

如果您只处理数据,则可以将其强制转换为接口(因为它纯粹是编译时结构),但这需要使用一个 TypeScript 类,该类使用数据实例并对该数据执行操作。

以下是一些复制数据的示例:

  1. 将Ajax JSON对象复制到现有对象中
  2. 将JSON字符串解析为JavaScript中特定的对象原型

本质上,你只需要 :

var d = new MyRichObject();
d.copyInto(jsonResult);

1
当然,那也可能行--我不知道它是否会更有效率,因为它需要为每个属性调用额外的函数调用。 - WiredPrairie
2
绝对不是我想要的答案:( 出于好奇,为什么会这样呢?在我看来,JavaScript的工作方式应该可以做到这一点。 - David Thielen
1
Object.setPrototypeOf 怎么样? - Petah
6
我刚刚执行了 Object.assign(new ApiAuthData(), JSON.parse(rawData)) - Sam
1
只要你的类专门用于接收纯JSON解码数据类型,它们就会成为对象的基本包装器(具有1对1的getter和setter)。对象将作为引用传递给构造函数,因此不会进行内存复制,除了一些内存地址(以及新的包装对象,这不会太多)。如果您控制所有类,则完全可行(我已经为一个私人大型项目完成了这项工作,效果很好)。您可以使用标准的.toJSON()方法控制重新序列化回JSON,只需设置返回存储的原始对象的私有引用即可。 - oxygen
显示剩余4条评论

184

我曾经遇到同样的问题,后来找到了一个能够解决这个问题的库:https://github.com/pleerock/class-transformer

它的使用方法如下:

let jsonObject = response.json() as Object;
let fooInstance = plainToClass(Models.Foo, jsonObject);
return fooInstance;

它支持嵌套子元素,但您需要对类的成员进行修饰。


22
这个精巧的小库以最少的努力完美地解决了这个问题(不要忘记你的@Type注解)。这个答案值得更多的赞扬。 - Benny Bottema
4
请注意,这个库几乎没有维护了。它不再与 Angular5+ 兼容,而且由于他们甚至不再合并拉取请求,我认为他们不会很快解决这个问题。尽管如此,它仍然是一个很棒的库。 - kentor
2
这在Angular 6中完全正常工作(至少对于我的用例,即直接映射JSON <=> Class)。 - tftd
2
使用Angular8+进行开发,而且正在积极开发中。对我来说,这是目前最重要的实用库之一。 - Tuomas Valtonen
1
在 Angular 9 中运行得非常好(不要忘记装饰所有嵌套的子组件并在您的 App.component.ts 中导入 import "reflect-metadata";)。 - Quentin Grisel
显示剩余5条评论

73
在TypeScript中,你可以使用接口和泛型进行类型断言,方法如下:

type assertion

var json = Utilities.JSONLoader.loadFromFile("../docs/location_map.json");
var locations: Array<ILocationMap> = JSON.parse(json).location;

ILocationMap描述了数据的形状。这种方法的优点是,您的JSON可能包含更多的属性,但形状满足接口的条件。

然而,这并不会添加类实例方法。


58
FYI:这是一种类型断言,而不是转换。 - WiredPrairie
6
请点击这里了解“类型断言”和“类型转换”的区别。 - Stefan Hanke
10
我该在哪里找到Utilities.JSONLoader? - HypeXR
31
如答案所述,它不会有任何方法。 - Martín Coll
4
主要的重点是能够使用在该类型中实现的方法(或方法)。 - Must.Tek
显示剩余2条评论

57

如果您正在使用ES6,请尝试以下方法:

class Client{
  name: string

  displayName(){
    console.log(this.name)
  }
}

service.getClientFromAPI().then(clientData => {
  
  // Here the client data from API only have the "name" field
  // If we want to use the Client class methods on this data object we need to:
  let clientWithType = Object.assign(new Client(), clientData)

  clientWithType.displayName()
})

但是不幸的是,这种方法无法在嵌套对象上起作用


9
他们要求使用Typescript。 - joe.feser
1
嗨@joe.feser,我提到ES6是因为这种方式需要使用“Object.assign”方法。 - 小广东
2
如果默认构造函数不存在,则可以通过Object.create(MyClass.prototype)创建目标实例,完全绕过构造函数。 - Marcello
2
有关嵌套对象限制的更多解释,请参见https://dev59.com/gmAh5IYBdhLWcg3wDfkW#O7GhEYcBWogLw_1b1DgT。 - Michael Freidgeim

36

我发现了一篇非常有趣的文章,关于将JSON通用转换为TypeScript类:

http://cloudmark.github.io/Json-Mapping/

最终代码如下:

let example = {
                "name": "Mark", 
                "surname": "Galea", 
                "age": 30, 
                "address": {
                  "first-line": "Some where", 
                  "second-line": "Over Here",
                  "city": "In This City"
                }
              };

MapUtils.deserialize(Person, example);  // custom class

这篇关于 JSON 映射器的文章链接是一篇不错的阅读材料。 - Cymatical

32

目前还没有自动检查从服务器接收到的JSON对象是否符合TypeScript接口属性预期(即符合)。但是您可以使用用户定义的类型保护

考虑以下接口和愚蠢的JSON对象(它可以是任何类型):

interface MyInterface {
    key: string;
 }

const json: object = { "key": "value" }

三种可能的方法:

A. 在变量后放置类型断言或简单的静态转换

const myObject: MyInterface = json as MyInterface;

B. 简单静态转换,在变量前和钻石符号之间

const myObject: MyInterface = <MyInterface>json;

C.高级动态转换,您可以检查对象的结构

function isMyInterface(json: any): json is MyInterface {
    // silly condition to consider json as conform for MyInterface
    return typeof json.key === "string";
}

if (isMyInterface(json)) {
    console.log(json.key)
}
else {
        throw new Error(`Expected MyInterface, got '${json}'.`);
}

你可以在这里玩弄这个例子

注意,这里的难点在于编写isMyInterface函数。我希望TS能够尽快添加一个装饰器,将复杂类型导出到运行时,并在需要时让运行时检查对象的结构。目前,您可以使用JSON模式验证器,其目的大致相同,或者使用运行时类型检查函数生成器

30

简述:一句话

// This assumes your constructor method will assign properties from the arg.
.map((instanceData: MyClass) => new MyClass(instanceData));

详细答案

不建议使用 Object.assign 方法,因为它可能会在类实例中添加一些不相关的属性(以及未在类本身中声明的封闭函数)。

在你想反序列化的类中,我建议确保任何想要反序列化的属性都被定义(为空值、空数组等)。通过用初始值定义属性,你可以在尝试迭代类成员为其赋值时暴露它们的可见性(请参见下面的 deserialize 方法)。

export class Person {
  public name: string = null;
  public favoriteSites: string[] = [];

  private age: number = null;
  private id: number = null;
  private active: boolean;

  constructor(instanceData?: Person) {
    if (instanceData) {
      this.deserialize(instanceData);
    }
  }

  private deserialize(instanceData: Person) {
    // Note this.active will not be listed in keys since it's declared, but not defined
    const keys = Object.keys(this);

    for (const key of keys) {
      if (instanceData.hasOwnProperty(key)) {
        this[key] = instanceData[key];
      }
    }
  }
}

在上面的例子中,我只是创建了一个反序列化方法。在真实世界的例子中,我会将其集中在可重用的基类或服务方法中。

以下是如何在像http resp之类的东西中使用它的方法...

this.http.get(ENDPOINT_URL)
  .map(res => res.json())
  .map((resp: Person) => new Person(resp) ) );

如果 tslint/ide 抱怨参数类型不兼容,请使用尖括号 <YourClassName> 将参数强制转换为相同类型,例如:

const person = new Person(<Person> { name: 'John', age: 35, id: 1 });

如果你有某些类成员属于特定类型(即另一个类的实例),那么你可以通过getter/setter方法将它们强制转换为具有类型的实例。

export class Person {
  private _acct: UserAcct = null;
  private _tasks: Task[] = [];

  // ctor & deserialize methods...

  public get acct(): UserAcct {
    return this.acct;
  }
  public set acct(acctData: UserAcct) {
    this._acct = new UserAcct(acctData);
  }

  public get tasks(): Task[] {
    return this._tasks;
  }

  public set tasks(taskData: Task[]) {
    this._tasks = taskData.map(task => new Task(task));
  }
}

上面的示例将同时将acct和任务列表反序列化为它们各自的类实例。


我收到了这个错误信息: 类型 '{ name: string, age: number, id: number }' 无法转换为类型 'Person'。在类型 'Person' 中,属性 'id' 是私有的,但在类型 '{ name: string, age: number, id: number }' 中不是。 - utiq
我应该如何在枚举中使用它?我是否需要采用特定类型的方法并添加getter和setter? - Tadija Bagarić
@ TimothyParez,你什么时候设置任务? - Kay
我尝试做类似的事情,但当我console.log person时,我的tasks数组为空。 - Kay
为了编译这个,我不得不在类中添加一个索引签名:export class Person { [key: string]: any (...) } - Dawied

19

假设JSON对象的属性与TypeScript类相同,您无需将Json属性复制到TypeScript对象中。您只需要通过构造函数传递json数据来构造您的TypeScript对象。

在您的Ajax回调中,您会收到一个公司:

onReceiveCompany( jsonCompany : any ) 
{
   let newCompany = new Company( jsonCompany );

   // call the methods on your newCompany object ...
}

为了使其工作:

1)在您的Typescript类中添加一个构造函数,该构造函数以json数据为参数。在该构造函数中,您可以使用jQuery扩展您的json对象,例如:$.extend( this, jsonData)。 $.extend允许保留javascript原型并添加json对象的属性。

2)请注意,您还必须对链接对象执行相同的操作。在示例中的Employees的情况下,您还创建了一个构造函数,该函数使用员工json数据的一部分。您调用$.map将json员工转换为typescript Employee对象。

export class Company
{
    Employees : Employee[];

    constructor( jsonData: any )
    {
        $.extend( this, jsonData);

        if ( jsonData.Employees )
            this.Employees = $.map( jsonData.Employees , (emp) => {
                return new Employee ( emp );  });
    }
}

export class Employee
{
    name: string;
    salary: number;

    constructor( jsonData: any )
    {
        $.extend( this, jsonData);
    }
}

当处理Typescript类和JSON对象时,我发现这是最好的解决方案。


我更喜欢这个解决方案,而不是实现和维护接口,因为我的Angular2应用程序具有真正的应用程序模型,可能与我的应用程序消耗的网络服务的模型不同。它可以拥有私有数据和计算属性。 - Anthony Brenelière
9
在Angular项目中使用JQuery是一个糟糕的想法。如果你的模型包含了许多函数,那它们就不再是模型了。 - Davor
1
@Davor 你是指POJO还是模型?POJO(基本上是普通对象)没有函数,而模型是一个更广泛的术语,它包括存储库。相对于POJO,存储库模式涉及到函数,但它仍然是模型。 - forsberg
@Davor:在Angular项目中使用JQuery并不是一个坏主意,只要你不使用它来操作DOM,这确实是一个可怕的想法。我会在我的Angular项目中使用任何我需要的库,而对于jQuery来说,这不是一个选项,因为我的项目使用依赖于它的SignalR。对于ES6现在使用的类,数据是通过封装数据存储方式的函数属性进行访问的。对于构造函数,有一种适当的方式是使用工厂。 - Anthony Brenelière
1
这篇文章明显是关于REST的纯数据模型。你们让它变得不必要地复杂了。当然,你可以使用Jquery来进行其他操作,但是导入一个庞大的库只为使用其中1%的功能,这是代码异味的体现,我从未见过这样的情况。 - Davor

17

在我的案例中,它起作用了。我使用了函数 Object.assign(target,sources ...)。 首先,创建正确的对象,然后将数据从JSON对象复制到目标中。示例:

let u:User = new User();
Object.assign(u , jsonUsers);

还有一个更高级的使用示例。一个使用数组的例子。

this.someService.getUsers().then((users: User[]) => {
  this.users = [];
  for (let i in users) {
    let u:User = new User();
    Object.assign(u , users[i]);
    this.users[i] = u;
    console.log("user:" + this.users[i].id);
    console.log("user id from function(test it work) :" + this.users[i].getId());
  }

});

export class User {
  id:number;
  name:string;
  fullname:string;
  email:string;

  public getId(){
    return this.id;
  }
}

当您拥有一个私有属性时会发生什么? - prasanthv
因为jsonUser对象不是User类,所以没有使用Object.assign(u, jsonUsers)操作,您无法使用getId()函数。 只有在分配后,您才能获得有效的User对象,从而可以使用getId()函数。 getId()函数仅用于说明操作成功的示例。 - Adam111p
你可以跳过临时变量,直接使用 this.users[i] = new User(); Object.assign(this.users[i], users[i]) - cyptus
2
甚至更好的是利用返回值:this.users[i] = Object.assign(new User(), users[i]); - cyptus
这个长版本仅用于解释。您可以尽可能缩短代码 :) - Adam111p

9

虽然它不是直接地进行类型转换,但我发现https://github.com/JohnWhiteTB/TypedJSON 是一个有用的选择。

@JsonObject
class Person {
    @JsonMember
    firstName: string;

    @JsonMember
    lastName: string;

    public getFullname() {
        return this.firstName + " " + this.lastName;
    }
}
var person = TypedJSON.parse('{ "firstName": "John", "lastName": "Doe" }', Person);

person instanceof Person; // true
person.getFullname(); // "John Doe"

1
它不是强制类型转换,它真正做了什么? - DanielM
1
这个解决方案需要大量的注释。难道真的没有更简单的方法吗? - JPNotADragon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接