DEV Community

Cover image for Reviving JSON classes made easy
Philippe Poulard
Philippe Poulard

Posted on

Reviving JSON classes made easy

JSON.parse() is the standard Javascript function that one use to parse some JSON text to a plain Javascript object or array.

But what you get is only a mix of plain objects, arrays, strings, numbers, booleans or nulls value. If the data source were made of instance of classes, or just dates objects, you are done. You can't recover them efficiently.

However, JSON.parse() accept as a second argument a revive function that is invoked during parsing for each key-value pair encountered, but what you have is only the key name regardless of any context : where are you in the hierarchy of objects ? For arrays it is worse : if the key is "0", which array of your structure are we talking about if you have many ? Really, you are done...

What we need is to design a reviver that takes care of the hierarchy of the target data structure. We want it simple to use. This is the job of @badcafe/jsonizer, a Typescript library that helps you revive any JSON data to an arbitrary hierarchy of plain objects, arrays, and class instances.

Let's start with an example :

{
    "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"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

This is a typical JSON file that contains Date instances that were stringified. JSON.parse() will give you... strings only. But Jsonizer let you express the expected mapping to revive date instances :

const personReviver = Jsonizer.reviver({
    birthDate: Date,
    hobbies: {
        '*': {
            startDate: Date
        }
    }
});
// personJson contains the JSON string displayed previously
const personFromJson = JSON.parse(personJson, personReviver);
Enter fullscreen mode Exit fullscreen mode

It's easy to understand that Jsonizer's reviver function takes as an argument a plain object that contains a mapping for fields that have to be transformed (we should say 'augmented' to typed data) ; there is also an entry that matches any array item '*' inside the hobbies array ! ('*' looks familiar isn't it ?)

This is where Jsonizer shines : it is able to take care of the hierarchy making any single mapping contextual, and the mapper is really simple to express.

Let's go on. Say that a hobby is a class ; how Jsonizer let us define a reviver ? For classes, we will use the decorator @Reviver :

@Reviver<Hobby>({ // 👈  bind the reviver to the class
    '.': ({hobby, startDate}) => new Hobby(hobby, startDate),
    // 👆 instance builder
    startDate: Date
})
class Hobby {
    constructor(
        public hobby: string,
        public startDate: Date
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
  • we switched to Typescript, but the example also works in Javascript: just remove type infos in the code and write the fields assignment in the constructor... or consider it's time to move to Typescript !
  • the decorator function @Reviver let us decorate our class with once again a mapper, but this mapper has a special entry '.' the (looks familiar) "self" entry that indicates how to create an instance of Hobby
  • oh ! another advantage of using Typescript is that the mapper is constrained to fields that exist in the source data structure, so it helps you to define correct mappings easily.

Now, we can refer it in our first mapping :

const personReviver = Jsonizer.reviver({
    birthDate: Date,
    hobbies: {
        '*': Hobby // 👈  we can refer a class
                   //     decorated with @Reviver
    }
});
Enter fullscreen mode Exit fullscreen mode

After parsing, hobbies will contain an array of Hobby instances !

Fine... but what about Date ? Well, it is so common that Jsonizer ships its reviver ; so you can just use it.

Finally, we could also define a Person class and bound it to a reviver :

@Reviver<Person>({
    '.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), 
    birthDate: Date,
    hobbies: {
        '*': Hobby
    }
})
class Person {
    constructor(
        public name: string,
        public birthDate: Date,
        public hobbies: Hobby[]
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Using it is also simple :

const personReviver = Reviver.get(Person); // 👈  extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);
// this is 👆 an instance of Person
Enter fullscreen mode Exit fullscreen mode

Thanks for reading ! I invite you to have a look at @badcafe/jsonizer: you will find more outstanding features !

Top comments (0)