ChatGPT解决这个技术问题 Extra ChatGPT

How do I cast a JSON Object to a TypeScript class?

I read a JSON object from a remote REST server. This JSON object has all the properties of a typescript class (by design). How do I cast that received JSON object to a type var?

I don't want to populate a typescript var (ie have a constructor that takes this JSON object). It's large and copying everything across sub-object by sub-object & property by property would take a lot of time.

Update: You can however cast it to a typescript interface!

you can use github.com/vojtechhabarta/typescript-generator to generate TypeScript interfaces in case your JSON is mapped using Java classes
I've made a tool for this beshanoe.github.io/json2ts
Creating prototype TypeScript class to define your object won't hurt real production code. Take a look at the compiled JS file, all the definitions will be removed, since they are not part of JS.
I don't consider any of the solutions on StackOverflow to be a comprehensive solution to the problem. So, I created an npm package angular-http-deserializer for this: npmjs.com/package/angular-http-deserializer#usage

C
Community

You can't simple cast a plain-old-JavaScript result from an Ajax request into a prototypical JavaScript/TypeScript class instance. There are a number of techniques for doing it, and generally involve copying data. Unless you create an instance of the class, it won't have any methods or properties. It will remain a simple JavaScript object.

While if you only were dealing with data, you could just do a cast to an interface (as it's purely a compile time structure), this would require that you use a TypeScript class which uses the data instance and performs operations with that data.

Some examples of copying the data:

Copying AJAX JSON object into existing Object Parse JSON String into a Particular Object Prototype in JavaScript

In essence, you'd just :

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

I agree with your answer. As an addition, although I'm not in a place to look it up and test it right now, I think those two steps could be combined by giving a wakeup function as a param to JSON.parse(). Both would still need to be done, but syntactically they could be combined.
Sure, that might work too -- I don't have a sense of whether it would be any more efficient though as it would need to call an extra function call for each property.
Definitely not the answer I was looking for :( Out of curiosity why is this? It seems to me the way javascript works that this should be doable.
What about Object.setPrototypeOf
I've just been doing Object.assign(new ApiAuthData(), JSON.parse(rawData))
s
s.m.

I had the same issue and I have found a library that does the job : https://github.com/pleerock/class-transformer.

It works like this :

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

It supports nested children but you have to decorate your class's member.


This brilliant little library solved it perfectly with the least of effort (don't forget your @Type annotations, though). This Answer deserves more credit.
Take note that this library is barely maintained anymore. It doesn't work with Angular5+ anymore and as they aren't even merging pull requests anymore, I don't think they are going to work on that anytime soon. It's a great library though.
This works just fine in Angular 6 (at least for my use case which is just to literally map JSON <=> Class)
Working with angular8+ and is in active development. For me this is one of the most important utility libraries out there
Works like a charm in Angular 9 (Don't forget to decorate all nested child and import import "reflect-metadata"; in your App.component.ts).
j
janniks

In TypeScript you can do a type assertion using an interface and generics like so:

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

Where ILocationMap describes the shape of your data. The advantage of this method is that your JSON could contain more properties but the shape satisfies the conditions of the interface.

However, this does NOT add class instance methods.


FYI: It's a type assertion, not a cast.
See here for the difference between a type assertion and a cast.
Where can I find Utilities.JSONLoader?
But it won't have any methods, as mentioned in the answer.
The main point is able to use method(s) which is implemented in the type.
j
janniks

If you are using ES6, try this:

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

But this method will not work on nested objects, sadly.


They asked for it in Typescript.
HI @joe.feser , I mention ES6 because this way the 'Object.assign' method is required.
In case the default constructor is missing, the target instance can be created through Object.create(MyClass.prototype), bypassing the constructor altogether.
More explanation about nested objects limitation see in stackoverflow.com/questions/22885995/…
P
Philip

I found a very interesting article on generic casting of JSON to a Typescript Class:

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

You end up with following code:

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

's JSON mapper article link is good read.
T
Timothy Perez

TLDR: One liner

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

The Detailed Answer

I would not recommend the Object.assign approach, as it can inappropriately litter your class instance with irrelevant properties (as well as defined closures) that were not declared within the class itself.

In the class you are trying to deserialize into, I would ensure any properties you want deserialized are defined (null, empty array, etc). By defining your properties with initial values you expose their visibility when trying to iterate class members to assign values to (see deserialize method below).

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

In the example above, I simply created a deserialize method. In a real world example, I would have it centralized in a reusable base class or service method.

Here is how to utilize this in something like an http resp...

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

If tslint/ide complains about argument type being incompatible, just cast the argument into the same type using angular brackets <YourClassName>, example:

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

If you have class members that are of a specific type (aka: instance of another class), then you can have them casted into typed instances through getter/setter methods.

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

The above example will deserialize both acct and the list of tasks into their respective class instances.


I get this error message: Type '{ name: string, age: number, id: number }' cannot be converted to type 'Person'. Property 'id' is private in type 'Person' but not in type '{ name: string, age: number, id: number }'
How should I use this with enums? Do I have to use the specific type approach and add getter and setter for it?
@TimothyParez When do you set the tasks?
I tried to do something similar but my tasks array is empty when i console.log person.
For this to compile I had to add an Index Signature to the class: export class Person { [key: string]: any (...) }
F
Flavien Volken

There is nothing yet to automatically check if the JSON object you received from the server has the expected (read is conform to the) typescript's interface properties. But you can use User-Defined Type Guards

Considering the following interface and a silly json object (it could have been any type):

interface MyInterface {
    key: string;
 }

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

Three possible ways:

A. Type Assertion or simple static cast placed after the variable

const myObject: MyInterface = json as MyInterface;

B. Simple static cast, before the variable and between diamonds

const myObject: MyInterface = <MyInterface>json;

C. Advanced dynamic cast, you check yourself the structure of the object

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}'.`);
}

You can play with this example here

Note that the difficulty here is to write the isMyInterface function. I hope TS will add a decorator sooner or later to export complex typing to the runtime and let the runtime check the object's structure when needed. For now, you could either use a json schema validator which purpose is approximately the same OR this runtime type check function generator


A
Anthony Brenelière

Assuming the json has the same properties as your typescript class, you don't have to copy your Json properties to your typescript object. You will just have to construct your Typescript object passing the json data in the constructor.

In your ajax callback, you receive a company:

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

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

In in order to to make that work:

1) Add a constructor in your Typescript class that takes the json data as parameter. In that constructor you extend your json object with jQuery, like this: $.extend( this, jsonData). $.extend allows keeping the javascript prototypes while adding the json object's properties.

2) Note you will have to do the same for linked objects. In the case of Employees in the example, you also create a constructor taking the portion of the json data for employees. You call $.map to translate json employees to typescript Employee objects.

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

This is the best solution I found when dealing with Typescript classes and json objects.


I prefer this solution over implementing and maintaining interfaces, because my Angular2 applications have a real application model that may be different to the model of the webservices my application consumes. It can have private datas and calculated properties.
Using JQuery in Angular projects is a terrible idea. And if your models contain a bunch of functions on them, they are not models anymore.
@Davor You mean POJO, or model? POJO (basically plain objects) have no functions, while model is a wider term, and it includes repository. Repository pattern, in contrast to POJO, is about functions, but it's still model.
@Davor: using JQuery in Angular projects is not a bad idea as long as you do not use it to manipulate the DOM, which is indeed a terrible idea. I do use any library I need for my Angular projects, and for jQuery it is not an option because my project uses SignalR that depends on it. In case of classes, now used by javascript ES6, data is accessed with properties that are function that encapsulate the way the data is stored in memory. For constructors, there is a proper way which using factories.
The OP is clearly about plain data models for REST. You're making it needlessly complicated, guys. And yeah, you can use Jquery for additional stuff, but you're importing a massive library to use 1% of it. That is a code smell if I've ever seen one.
J
Julian

In my case it works. I used functions Object.assign (target, sources ...). First, the creation of the correct object, then copies the data from json object to the target.Example :

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

And a more advanced example of use. An example using the array.

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

What happens when you have a private property?
Because the jsonUser object is not a User class. Without operation Object.assign (u, jsonUsers); You can not use the getId() function. Only after assign you get a valid User object in which you can use the getId() function. The getId() function is only for the example that the operation was successful.
you can skip the temp var - just do this.users[i] = new User(); Object.assign(this.users[i], users[i])
or even better make use of the return value: this.users[i] = Object.assign(new User(), users[i]);
This long version is for explanation only. You can shorten the code as much as you like :)
B
Bernoulli IT

While it is not casting per se; I have found https://github.com/JohnWhiteTB/TypedJSON to be a useful alternative.

@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"

It not casting, what does it really do?
This solution requires an awful lot of annotations. Is there really no easier way?
R
Rodney P. Barbati

Personally I find it appalling that typescript does not allow an endpoint definition to specify the type of the object being received. As it appears that this is indeed the case, I would do what I have done with other languages, and that is that I would separate the JSON object from the class definition, and have the class definition use the JSON object as its only data member.

I despise boilerplate code, so for me it is usually a matter of getting to the desired result with the least amount of code while preserving type.

Consider the following JSON object structure definitions - these would be what you would receive at an endpoint, they are structure definitions only, no methods.

interface IAddress {
    street: string;
    city: string;
    state: string;
    zip: string;
}

interface IPerson {
    name: string;
    address: IAddress;
}

If we think of the above in object oriented terms, the above interfaces are not classes because they only define a data structure. A class in OO terms defines data and the code that operates on it.

So we now define a class that specifies data and the code that operates on it...

class Person {
    person: IPerson;

    constructor(person: IPerson) {
        this.person = person;
    }

    // accessors
    getName(): string {
        return person.name;
    }

    getAddress(): IAddress {
        return person.address;
    }

    // You could write a generic getter for any value in person, 
    // no matter how deep, by accepting a variable number of string params

    // methods
    distanceFrom(address: IAddress): float {
        // Calculate distance from the passed address to this persons IAddress
        return 0.0;
    }
}

And now we can simply pass in any object conforming to the IPerson structure and be on our way...

   Person person = new Person({
            name: "persons name",
            address: {
                street: "A street address",
                city: "a city",
                state: "a state",
                zip: "A zipcode"
            }
        });

In the same fashion we can now process the object received at your endpoint with something along the lines of...

Person person = new Person(req.body);    // As in an object received via a POST call

person.distanceFrom({ street: "Some street address", etc.});

This is much more performant and uses half the memory of copying the data, while significantly reducing the amount of boilerplate code you must write for each entity type. It simply relies on the type safety provided by TypeScript.


The only sideback of this approach is that when you JSON.strigify(person), the result will not be the same as the plain JSON. In other words serialization produces a different output than deserialization.
@TiagoStapenhorstMartins But you shouldn't need to stringify the instance of the class. You could just JSON.stringify(person.person) - ie. the person property of the object. (I think, right?)
@rmcsharry yes, correct! well if you don't mind having the person object nested inside another object this solution is ok.
In actual use, if you needed to export the IPerson inside the Person class to JSON - the code performing the export would most likely be found in the Person class, perhaps as a getPersonJSON() method.
To go one step further, if you understand encapsulation and the value it brings, ideally, no other code would be accessing the IPerson inside Person - only the code in Person should be accessing the IPerson. Everything you might want to do to an IPerson should be done in the Person class - things like save(), delete(), etc.
D
Daniel Valdebenito

Use 'as' declaration:

const data = JSON.parse(response.data) as MyClass;

This technique was mentioned in this answer from two years prior, and as has been discussed elsewhere, does not add any functions that may be declared on MyClass.
I however think this is very efficient to use this for Model objects which generally do not have functions.
The only reason you would do this is in preparation to perform processing on the resulting object - indicating a need for a method of converting that provides the functions as well. See my answer above, which provides the equivalent of the above and the functions.
This does not work for empty object. Such parsing will return plain Object
R
Robert Christopher

Use a class extended from an interface.

Then:

    Object.assign(
        new ToWhat(),
        what
    )

And best:

    Object.assign(
        new ToWhat(),
        <IDataInterface>what
    )

ToWhat becomes a controller of DataInterface


N
Normunds Kalnberzins

An old question with mostly correct, but not very efficient answers. This what I propose:

Create a base class that contains init() method and static cast methods (for a single object and an array). The static methods could be anywhere; the version with the base class and init() allows easy extensions afterwards.

export class ContentItem {
    // parameters: doc - plain JS object, proto - class we want to cast to (subclass of ContentItem)
    static castAs<T extends ContentItem>(doc: T, proto: typeof ContentItem): T {
        // if we already have the correct class skip the cast
        if (doc instanceof proto) { return doc; }
        // create a new object (create), and copy over all properties (assign)
        const d: T = Object.create(proto.prototype);
        Object.assign(d, doc);
        // reason to extend the base class - we want to be able to call init() after cast
        d.init(); 
        return d;
    }
    // another method casts an array
    static castAllAs<T extends ContentItem>(docs: T[], proto: typeof ContentItem): T[] {
        return docs.map(d => ContentItem.castAs(d, proto));
    }
    init() { }
}

Similar mechanics (with assign()) have been mentioned in @Adam111p post. Just another (more complete) way to do it. @Timothy Perez is critical of assign(), but imho it is fully appropriate here.

Implement a derived (the real) class:

import { ContentItem } from './content-item';

export class SubjectArea extends ContentItem {
    id: number;
    title: string;
    areas: SubjectArea[]; // contains embedded objects
    depth: number;

    // method will be unavailable unless we use cast
    lead(): string {
        return '. '.repeat(this.depth);
    }

    // in case we have embedded objects, call cast on them here
    init() {
        if (this.areas) {
            this.areas = ContentItem.castAllAs(this.areas, SubjectArea);
        }
    }
}

Now we can cast an object retrieved from service:

const area = ContentItem.castAs<SubjectArea>(docFromREST, SubjectArea);

All hierarchy of SubjectArea objects will have correct class.

A use case/example; create an Angular service (abstract base class again):

export abstract class BaseService<T extends ContentItem> {
  BASE_URL = 'http://host:port/';
  protected abstract http: Http;
  abstract path: string;
  abstract subClass: typeof ContentItem;

  cast(source: T): T {
    return ContentItem.castAs(source, this.subClass);
  }
  castAll(source: T[]): T[] {
    return ContentItem.castAllAs(source, this.subClass);
  }

  constructor() { }

  get(): Promise<T[]> {
    const value = this.http.get(`${this.BASE_URL}${this.path}`)
      .toPromise()
      .then(response => {
        const items: T[] = this.castAll(response.json());
        return items;
      });
    return value;
  }
}

The usage becomes very simple; create an Area service:

@Injectable()
export class SubjectAreaService extends BaseService<SubjectArea> {
  path = 'area';
  subClass = SubjectArea;

  constructor(protected http: Http) { super(); }
}

get() method of the service will return a Promise of an array already cast as SubjectArea objects (whole hierarchy)

Now say, we have another class:

export class OtherItem extends ContentItem {...}

Creating a service that retrieves data and casts to the correct class is as simple as:

@Injectable()
export class OtherItemService extends BaseService<OtherItem> {
  path = 'other';
  subClass = OtherItem;

  constructor(protected http: Http) { super(); }
}

J
Jayant Varshney

You can create an interface of your type (SomeType) and cast the object in that.

const typedObject: SomeType = <SomeType> responseObject;

s
s_bighead

If you need to cast your json object to a typescript class and have its instance methods available in the resulting object you need to use Object.setPrototypeOf, like I did in the code snippet bellow:

Object.setPrototypeOf(jsonObject, YourTypescriptClass.prototype)

A
Amir Mehrabi

https://jvilk.com/MakeTypes/

you can use this site to generate a proxy for you. it generates a class and can parse and validate your input JSON object.


N
Nilupul Heshan

FOR JAVA LOVERS

Make POJO class

export default class TransactionDTO{
    constructor() {
    }
}

create empty object by class

let dto = new TransactionDto()   // ts object
let json = {name:"Kamal",age:40} // js object

let transaction: TransactionDto = Object.assign(dto,JSON.parse(JSON.stringify(json)));//conversion

M
Mac Chibueze

I used this library here: https://github.com/pleerock/class-transformer

<script lang="ts">
    import { plainToClass } from 'class-transformer';
</script>

Implementation:

private async getClassTypeValue() {
  const value = await plainToClass(ProductNewsItem, JSON.parse(response.data));
}

Sometimes you will have to parse the JSON values for plainToClass to understand that it is a JSON formatted data


The 'class-transformer' library is already suggested in other answer above stackoverflow.com/a/40042282/52277
J
Jaroslav

In the lates TS you can do like this:

const isMyInterface = (val: any): val is MyInterface => {
  if (!val) { return false; }
  if (!val.myProp) { return false; }
  return true;
};

And than user like this:

if (isMyInterface(data)) {
 // now data will be type of MyInterface
}

D
Derlin

I ran into a similar need. I wanted something that will give me easy transformation from/to JSON that is coming from a REST api call to/from specific class definition. The solutions that I've found were insufficient or meant to rewrite my classes' code and adding annotations or similars.

I wanted something like GSON is used in Java to serialize/deserialize classes to/from JSON objects.

Combined with a later need, that the converter will function in JS as well, I ended writing my own package.

It has though, a little bit of overhead. But when started it is very convenient in adding and editing.

You initialize the module with :

conversion schema - allowing to map between fields and determine how the conversion will be done Classes map array Conversion functions map - for special conversions.

Then in your code, you use the initialized module like :

const convertedNewClassesArray : MyClass[] = this.converter.convert<MyClass>(jsonObjArray, 'MyClass');

const convertedNewClass : MyClass = this.converter.convertOneObject<MyClass>(jsonObj, 'MyClass');

or , to JSON :

const jsonObject = this.converter.convertToJson(myClassInstance);

Use this link to the npm package and also a detailed explanation to how to work with the module: json-class-converter

Also wrapped it for
Angular use in : angular-json-class-converter


L
Lars Klingsten

Pass the object as is to the class constructor; No conventions or checks

interface iPerson {
   name: string;
   age: number;
}

class Person {
   constructor(private person: iPerson) { }

   toString(): string {
      return this.person.name + ' is ' + this.person.age;
   }  
}


// runs this as // 
const object1 = { name: 'Watson1', age: 64 };
const object2 = { name: 'Watson2' };            // age is missing

const person1 = new Person(object1);
const person2 = new Person(object2 as iPerson); // now matches constructor

console.log(person1.toString())  // Watson1 is 64
console.log(person2.toString())  // Watson2 is undefined

Q
QC-cheetah

You can use this npm package. https://www.npmjs.com/package/class-converter

It is easy to use, for example:

class UserModel {
  @property('i')
  id: number;

  @property('n')
  name: string;
}

const userRaw = {
  i: 1234,
  n: 'name',
};

// use toClass to convert plain object to class
const userModel = toClass(userRaw, UserModel);
// you will get a class, just like below one
// const userModel = {
//   id: 1234,
//   name: 'name',
// }

M
Mattia S.

You can with a single tapi.js! It's a lightweight automapper that works in both ways.

npm i -D tapi.js

Then you can simply do

let typedObject = new YourClass().fromJSON(jsonData)

or with promises

axios.get(...).as(YourClass).then(typedObject => { ... })

You can read more about it on the docs.


C
Cassio Seffrin

Instead a class may you could use a TypeScript interface.

Let's say we're working with a REST API that returns a JSON string representing a user. The JSON API agrees to a contract that it returns a string firstName, a string lastName and a number id. We model the user as an Interface in TypeScript as follow:

interface User {
   id: number;
   firstName: string;
   lastName: string;
}

The following code will be fine when you have the correct attributes map:

const json = '{"id": 1, "firstName": "Bob", "lastName": "Dylan"}'; //this will come from the REST API
try {
  const user: User = JSON.parse(json);
  console.log(user);
} catch (e) {
  console.log("An error on casting object has occurred", e);
}

Result:

[LOG]: {
  "id": 1,
  "firstName": "Bob",
  "lastName": "Dylan"
} 

Play this code here


S
SamazoOo

This is a simple and a really good option

let person = "{"name":"Sam","Age":"30"}";

const jsonParse: ((key: string, value: any) => any) | undefined = undefined;
let objectConverted = JSON.parse(textValue, jsonParse);

And then you'll have

objectConverted.name

a
ashkufaraz

You can cast json to property like this

class Jobs {
  constructor(JSONdata) {
    this.HEAT = JSONdata.HEAT;    
    this.HEAT_EAF = JSONdata.HEAT_EAF;    
  }
  
}

 var job = new Jobs({HEAT:'123',HEAT_EAF:'456'});