Reviving JSON classes made easy
Tags: typescript, javascript, json
Published: 2022-01-28 17:24:10

`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`](https://badcafe.github.io/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 :

```json
{
    "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"
        }
    ]
}
```

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 :

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

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` :

```typescript
@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
    ) {}
}
```

- 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 :

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

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 :

```typescript
@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[]
    ) {}
}
```

Using it is also simple :

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

Thanks for reading ! I invite you to have a look at [`@badcafe/jsonizer`](https://badcafe.github.io/jsonizer): you will find more outstanding features !