Are there any means for JSON serialization/deserialization of Typescript objects so that they don't lose type information? Simple JSON.parse(JSON.stringify)
has too many caveats.
Or I should use adhoc solutions?
Are there any means for JSON serialization/deserialization of Typescript objects so that they don't lose type information? Simple JSON.parse(JSON.stringify)
has too many caveats.
Or I should use adhoc solutions?
Use Interfaces to get strong types:
// Creating
var foo:any = {};
foo.x = 3;
foo.y='123';
var jsonString = JSON.stringify(foo);
alert(jsonString);
// Reading
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
And use type assertion "<>" if you need to.
I think a better way to handle this is to use Object.assign (which however requires ECMAScript 2015).
Given a class
class Pet {
name: string;
age: number;
constructor(name?: string, age?: number) {
this.name = name;
this.age = age;
}
getDescription(): string {
return "My pet " + this.name + " is " + this.age + " years old.";
}
static fromJSON(d: Object): Pet {
return Object.assign(new Pet(), d);
}
}
Serialize and deserialize like this...
var p0 = new Pet("Fido", 5);
var s = JSON.stringify(p0);
var p1 = Pet.fromJSON(JSON.parse(s));
console.log(p1.getDescription());
To take this example to the next level, consider nested objects...
class Type {
kind: string;
breed: string;
constructor(kind?: string, breed?: string) {
this.kind = kind;
this.breed = breed;
}
static fromJSON(d: Object) {
return Object.assign(new Type(), d);
}
}
class Pet {
name: string;
age: number;
type: Type;
constructor(name?: string, age?: number) {
this.name = name;
this.age = age;
}
getDescription(): string {
return "My pet " + this.name + " is " + this.age + " years old.";
}
getFullDescription(): string {
return "My " + this.type.kind + ", a " + this.type.breed + ", is " + this.age + " years old.";
}
static fromJSON(d: Object): Pet {
var o = Object.assign(new Pet(), d);
o.type = Type.fromJSON(o['type']);
return o;
}
}
Serialize and deserialize like this...
var q0 = new Pet("Fido", 5);
q0.type = new Type("dog", "Pomeranian");
var t = JSON.stringify(q0);
var q1 = Pet.fromJSON(JSON.parse(t));
console.log(q1.getFullDescription());
So unlike using an interface, this approach preserves methods.
The best method I found so far was to use "jackson-js". jackson-js is a project that allows you to describe the class using ts-decorators and then serialize and desirialize saving the type information. It supports arrays, maps, etc.
Full tutorial: https://itnext.io/jackson-js-powerful-javascript-decorators-to-serialize-deserialize-objects-into-json-and-vice-df952454cf
Simple example:
import { JsonProperty, JsonClassType, JsonAlias, ObjectMapper } from 'jackson-js';
class Book {
@JsonProperty() @JsonClassType({type: () => [String]})
name: string;
@JsonProperty() @JsonClassType({type: () => [String]})
@JsonAlias({values: ['bkcat', 'mybkcat']})
category: string;
}
class Writer {
@JsonProperty() @JsonClassType({type: () => [Number]})
id: number;
@JsonProperty() @JsonClassType({type: () => [String]})
name: string;
@JsonProperty() @JsonClassType({type: () => [Array, [Book]]})
books: Book[] = [];
}
const objectMapper = new ObjectMapper();
// eslint-disable-next-line max-len
const jsonData = '{"id":1,"name":"John","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';
const writer = objectMapper.parse<Writer>(jsonData, {mainCreator: () => [Writer]});
console.log(writer);
/*
Writer {
books: [
Book { name: 'Learning TypeScript', category: 'Web Development' },
Book { name: 'Learning Spring', category: 'Java' }
],
id: 1,
name: 'John'
}
*/
There are a few other projects that claim to do the same thing -
However, jackson-js is the only one that worked for me when I used a TypeScript Map.
First, you need to create an interface of your source entity which you receive from the API as JSON:
interface UserEntity {
name: string,
age: number,
country_code: string
};
Second, implement your model with constructor where you can customize (camelize) some field names:
class User {
constructor({ name, age, country_code: countryCode }: UserEntity) {
Object.assign(this, { name, age, countryCode });
}
}
Last, create an instance of your User model using JavaScript object "jsonUser"
const jsonUser = {name: 'Ted', age: 2, country_code: 'US'};
const userInstance = new User(jsonUser);
console.log({ userInstance })
I would also suggests using ts-jackson
It is build with typescript in mind and allows to resolve deeply nested structures.
The AQuirky answer works for me. You may have some troubles with the Object.assign method. I had to modify my tsconfig.json to include:
"compilerOptions": {
...
"lib": ["es2015"],
...
}
AQuirky's answer is a good starting point, but as mentioned in my comment, its main problem is that it needs to allow creating objects with undefined fields, which are then populated by his fromJSON
method.
This violates the RAII principle, and can/will confuse users of that class who might fall into the trap of creating an incomplete Pet (nowhere is it explicit that calling the constructor without arguments must be followed by a call to fromJSON() to populate the object).
So building on his answer, here's one way, using JavaScript's prototype chain, to get back an object of a class after serializing/deserializing. The key trick is just to reassign the correct prototype object after serializing and deserializing:
class Foo {}
foo1 = new Foo();
foo2 = JSON.parse(JSON.stringify(p1))
foo2.__proto__ = Foo.prototype;
So to fix AQuirky's example using this trick, we could simply change his fromJSON
function to
static fromJSON(d: Object): Pet {
d.__proto__ = Pet.prototype;
return p
}