How can TypeScript objects be serialized?

Is there a reliable method for preserving type information during JSON serialization/deserialization of Typescript objects? The straightforward JSON.parse(JSON.stringify) approach has proven to have several limitations.

Are there more effective ad-hoc solutions available?

Answer №1

Utilize Interfaces for Strongly Typed Data:

// Creating 
var obj:any = {};
obj.x = 3;
obj.y='123';

var jsonData = JSON.stringify(obj);
alert(jsonData);


// Reading
interface CustomInterface{
    x:number;
    y?:string; 
}

var customObj:CustomInterface = JSON.parse(jsonData);
alert(customObj.y);

Remember to use type assertion "<>" when necessary.

Answer №2

A more efficient way to manage this situation is by utilizing Object.assign, which does require ECMAScript 2015.

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

Serialization and deserialization can be done 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 enhance this example further, let's delve into 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;
    }
}

Serialization and deserialization for this scenario would look like...

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

Therefore, unlike employing an interface, this method retains all the class methods.

Answer №3

In my experience, the most effective approach I have come across is utilizing "jackson-js". This project allows for the use of ts-decorators to define classes and then easily serialize and deserialize while preserving type information. It has robust support for arrays, maps, and more.

For a comprehensive guide on using jackson-js, check out:

Here's a 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'
}
*/

While there are other similar projects available, such as -

From my personal experience, jackson-js stands out as the only one that successfully handled TypeScript Maps.

Answer №4

To start, you must define an interface for the source entity received from the API in JSON format:

interface UserEntity {
  name: string,
  age: number,
  country_code: string
};

Next, create a model with a constructor to customize (camelize) certain field names:

class User {
  constructor({ name, age, country_code: countryCode }: UserEntity) {
    Object.assign(this, { name, age, countryCode });
  }
}

Finally, instantiate your User model using a JavaScript object named "jsonUser":

const jsonUser = {name: 'Ted', age: 2, country_code: 'US'};

const userInstance = new User(jsonUser);

console.log({ userInstance })

https://i.stack.imgur.com/NdYVY.png

Click here for playground.

Answer №5

For those seeking a solution, I recommend utilizing ts-jackson

This library is specifically designed with TypeScript in focus and excels in resolving complex nested structures.

Answer №6

AQuirky's response serves as a solid starting point, however, as I mentioned in my previous comment, an issue arises regarding the necessity to permit the creation of objects with undefined fields that are then filled by his fromJSON method.

This contravenes the RAII principle and may lead to confusion for users of that particular class who might inadvertently create an incomplete Pet (it is not explicitly stated anywhere that invoking the constructor without arguments requires subsequent calls to fromJSON() to complete the object).

To expand upon his solution, here is one approach involving JavaScript's prototype chain, in order to retrieve an object of a class post-serialization/deserialization. The main technique revolves around reassigning the correct prototype object after these processes:

class Foo {}
foo1 = new Foo();
foo2 = JSON.parse(JSON.stringify(p1))
foo2.__proto__ = Foo.prototype;

https://i.stack.imgur.com/JSZQ0.png

To rectify AQuirky's example using this strategy, we could easily modify his fromJSON function as follows:

static fromJSON(d: Object): Pet {
    d.__proto__ = Pet.prototype;
    return p
}

Answer №7

When working with Typescript, the @badcafe/jsonizer library offers a more concise alternative to ts-jackson. With , you only need to define mappings when necessary, such as when dealing with instances of classes rather than basic JS types:

npm install @badcafe/jsonizer

For example :

import { Reviver, Jsonizer } from '@badcafe/jsonizer';

@Reviver<Book>({
    // 👆  bind the reviver to the class
    '.': Jsonizer.Self.assign(Book)
    // 👆  '.' key is the Self builder, that will assign each field
})
class Book {
  name: string;
  category: string;
}

@Reviver<Writer>({
  '.': Jsonizer.Self.assign(Writer),
  birthDate: Date,
  books: {
    '*': Book
  }
})
class Writer {
  id: number;
  name: string;
  birthDate: Date;
  books: Book[] = [];
}

// eslint-disable-next-line max-len
const jsonData = '{"id":1,"name":"John","birthDate":"1990-12-31","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';

const writerReviver = Reviver.get(Writer);
// standard Javascript parse function :
const writer = JSON.parse(jsonData, writerReviver);
//     👆 it's an instance of Writer, and in the code, it's type is inferred to be a Writer (usually, it is TS any)

Here is another approach for handling variations:

import { Reviver } from '@badcafe/jsonizer';

interface BookDTO {
    name: string
    category?: string
    bkcat?: string
    mybkcat?: string
}

@Reviver<Book, BookDTO>({
    // 👇 this time, we have a custom builder
    '.': ({ name, category, bkcat, mybkcat}) => {
             const book = new Book();
             book.name = name;
             book.category = category ?? bkcat ?? mybkcat;
             return book;
         }
})
class Book {
  name: string;
  category: string;
}

Below, there is a variant for the Writer class:

  1. The Writer class may be defined in another library and imported, but Jsonizer allows binding a reviver even on imported classes.
  2. A constructor with fields arguments can be used instead.

Firstly, define the class:

export class Writer {
  constructor( 
    public id: number,
    public name: string,
    public birthDate: Date,
    public books: Book[] = []
  ) {}
}
import { Reviver, Jsonizer } from '@badcafe/jsonizer';
import { Writer, Book } from '...';

// 👇 it is no longer a decorator
Reviver<Writer>({
  '.': Jsonizer.Self.apply(Writer), // apply instead of assign
  birthDate: Date,
  books: {
    '*': Book
  }
})(Writer)

const jsonData = '{"id":1,"name":"John","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';

const writerReviver = Reviver.get(Writer);
const writer = JSON.parse(jsonData, writerReviver);

With builders helpers like Jsonizer.Self.assign() and Jsonizer.Self.apply(), as well as custom builders, recreating other structures like a Map is straightforward. To serialize it, simply define the standard toJSON() function in your class.

@badcafe/jsonizer offers additional capabilities such as:

  • In case of classes with the same name, assigning them a namespace to avoid clashes.
  • Techniques in the documentation demonstrate how to dynamically select the right reviver, send the reviver with payload data, and revive a reviver.

Answer №8

After trying out the solution from AQuirky's answer, I found it to be effective for my situation. I encountered some difficulties with the Object.assign method, but was able to resolve them by making adjustments to my tsconfig.json file:

"compilerOptions": {
    ...
    "lib": ["es2015"],
    ...
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieve the nearest attribute from a radio button when using JQuery on a click event

I have a query regarding the usage of JS / JQuery to extract the data-product-name from a selected radio button, prior to any form submission. Although my attempt at code implementation seems incorrect: return jQuery({{Click Element}}).closest("form" ...

Is it possible to share data from one controller in a JS file to another JS file? (using AngularJS)

I need to create a table in JavaScript. In one of my JS files, I have a controller with JSON data containing properties like width, height, color, etc. In another JS file, I am building the actual table structure. Here is an example of my AngularJS file: ...

Utilizing Spring and JPA to Store Data in a JSON Column within a Postgres Database

My database table "test" in Postgres 9.3 has a column named "sample_column" of type json that I'm trying to populate with {"name":"Updated name"} using Spring / JPA. After researching, I found that I need to implement a custom converter to map the st ...

Component re-rendering and initializing useReducer

I made some revisions to this post. Initially, I shared the entire problem with my architecture and later updated it to focus directly on the issue at hand in order to make it easier for the community to provide assistance. You can now jump straight to the ...

Can the color scheme be customized for both light and dark themes in Ant Design version 5?

After creating a hook that successfully toggles between light and dark modes in Chrome's Rendering panel, I noticed that the values in the ConfigProvider remain unchanged when switching themes. Can anyone suggest a workaround to modify the design toke ...

What is the best way to retrieve the ID of a post request using React's axios hook?

My goal is to make a post request to create an app, and then establish its links. To achieve this, I need to obtain the id of the newly created items in order to create the links. Is there a way to retrieve the id of the item created through the post reque ...

Save the content of a string object into an array

I am currently implementing an array sorting functionality using the MVC approach. The array called "array" is used to store values provided at runtime. Within the view, there is a textbox and a button for user input of integer values. Upon clicking the bu ...

Combining PHP code within JavaScript nested inside PHP code

I am currently facing an issue with my PHP while loop. The loop is supposed to iterate through a file, extract three variables for every three lines, and then use those variables to create markers using JavaScript. However, when I try to pass the PHP varia ...

What is the best way to update a canvas chart when the side menu is hidden?

I am facing an issue with the layout of my webpage, which includes a left side menu and a canvas chart. The left side menu occupies the first 155 pixels of the width, leaving the rest for the canvas chart set to a width of 100%. However, when I close the ...

Revamp your D3 interactive graph with real-time data updates from the server!

I am looking to enhance a graph in D3 (or another visualization library) by completing the following steps: When a user clicks on an item, like a node within the graph, New data is fetched from the server, The new data is then used to add new edges and n ...

Use jQuery's .each method to reiterate through only the initial 5 elements

Is there a way to loop through just the initial 5 elements using jQuery's each method? $(".kltat").each(function() { // Restrict this to only the first five elements of the .kltat class } ...

Here's a guide on how to package and send values in ReactJs bundles

I'm currently involved in a ReactJs project that does not rely on any API for data management. For bundling the React APP, we are using Webpack in the project. The challenge now is to make the React APP usable on any website by simply including the ...

Transform a group of objects in Typescript into a new object with a modified structure

Struggling to figure out how to modify the return value of reduce without resorting to clunky type assertions. Take this snippet for example: const list: Array<Record<string, string | number>> = [ { resourceName: "a", usage: ...

Verify if the input field is devoid of any content or not

I am planning to create a validation form using Vanilla JavaScript. However, I have encountered an issue. Specifically, I want to validate the 'entername' field first. If the user does not enter any letters in it, I would like to display the mess ...

VueJS 3 custom Checkbox fails to update UI upon clicking

I'm attempting to implement a customized checkbox using Vue 3 and the composition API based on this example. However, despite confirming through devtools that all my props and bound data are successfully passed from the parent component to the child c ...

Using Retrofit2 to display JSON data in a RecyclerView

I've been trying to use Retrofit2 to fetch JSON data and then parse it into my RecyclerView adapter without much success. Currently, I can populate one RecyclerView with local DBFlow data, but I'm struggling to do the same for the JSON data retri ...

Automatically switch to the designated tab depending on the URL

Is it possible to automatically activate a specific tab based on the URL structure if my tabs are configured in this way? I am looking for a solution where a link on www.example1.com/notabs.html will redirect to www.example2.com/tabs.html This particular ...

Experiencing unexpected behavior with React Redux in combination with Next.js and NodeJS

I'm in the process of developing an application using Next.js along with redux by utilizing this particular example. Below is a snippet from my store.js: // REDUCERS const authReducer = (state = null, action) => { switch (action.type){ ...

The issue of footer overlapping the login form is observed on iOS devices while using Safari and Chrome

Unique ImageI am currently working on an Angular 8 project with Angular Material. I have successfully designed a fully functional login page. However, I am encountering a problem specifically on iOS devices such as iPhones and iPads, whether it is Safari o ...

The requested property map cannot be found within the Array

I am working on a project using redux with react typescript. I have an external JSON file that contains employee data categorized by department id. To properly map the data with types in my application, I have created specific types. Check out this demo o ...