Not a new topic, but there is a new solution : the modern approach (in December 2021) is to use @badcafe/jsonizer
: https://badcafe.github.io/jsonizer
- Unlike other solutions, it doesn't pollute you data with injected class names,
- and it reifies the expected data hierarchy.
- below are some examples in Typescript, but it works as well in JS
Before showing an example with a class, let's start with a simple data structure :
const person = {
name: 'Bob',
birthDate: new Date('1998-10-21'),
hobbies: [
{ hobby: 'programming',
startDate: new Date('2021-01-01'),
},
{ hobby: 'cooking',
startDate: new Date('2020-12-31'),
},
]
}
const personJson = JSON.stringify(person);
// {
// "name": "Bob",
// "birthDate": "1998-10-21T00:00:00.000Z",
// "hobbies": [
// {
// "hobby": "programming",
// "startDate": "2021-01-01T00:00:00.000Z"
// },
// {
// "hobby": "cooking",
// "startDate": "2020-12-31T00:00:00.000Z"
// }
// ]
// }
// store or send the data
Notice that dates are serialized to strings, and if you parse that JSON, dates won't be Date
instances, they will be String
s
Now, let's use Jsonizer
// in Jsonizer, a reviver is made of field mappers :
const personReviver = Jsonizer.reviver<typeof person>({
birthDate: Date,
hobbies: {
'*': {
startDate: Date
}
}
});
const personFromJson = JSON.parse(personJson, personReviver);
Every dates string in the JSON text have been mapped to Date
objects in the parsed result.
Jsonizer can indifferently revive JSON data structures (arrays, objects) or class instances with recursively nested custom classes, third-party classes, built-in classes, or sub JSON structures (arrays, objects).
Now, let's use a class instead :
// in Jsonizer, a class reviver is made of field mappers + an instance builder :
@Reviver<Person>({ // bind the reviver to the class
'.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // instance builder
birthDate: Date,
hobbies: {
'*': {
startDate: Date
}
}
})
class Person {
constructor( // all fields are passed as arguments to the constructor
public name: string,
public birthDate: Date
public hobbies: Hobby[]
) {}
}
interface Hobby {
hobby: string,
startDate: Date
}
const person = new Person(
'Bob',
new Date('1998-10-21'),
[
{ hobby: 'programming',
startDate: new Date('2021-01-01'),
},
{ hobby: 'cooking',
startDate: new Date('2020-12-31'),
},
]
);
const personJson = JSON.stringify(person);
const personReviver = Reviver.get(Person); // extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);
Finally, let's use 2 classes :
@Reviver<Hobby>({
'.': ({hobby, startDate}) => new Hobby(hobby, startDate), // instance builder
startDate: Date
})
class Hobby {
constructor (
public hobby: string,
public startDate: Date
) {}
}
@Reviver<Person>({
'.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // instance builder
birthDate: Date,
hobbies: {
'*': Hobby // we can refer a class decorated with @Reviver
}
})
class Person {
constructor(
public name: string,
public birthDate: Date,
public hobbies: Hobby[]
) {}
}
const person = new Person(
'Bob',
new Date('1998-10-21'),
[
new Hobby('programming', new Date('2021-01-01')),
new Hobby('cooking', new Date('2020-12-31')
]
);
const personJson = JSON.stringify(person);
const personReviver = Reviver.get(Person); // extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);