How to fix a bad JSON structure
Tags: typescript, javascript, json, jsonizer
Published: 2022-02-07 12:50:19

Sometimes, you receive external data that are not well designed, like shown in the following JSON file with comments :

```json
{
  "firstName": "Bob",
  "numberOfHobbies": "3",         // 👈  should be a number
  "birthDate": "21/10/1998",      // 👈  formatted Date
  "hobbies": "cooking,skiing,programming" // 👈  not JSON-friendly
}
```

But you prefer a clean target structure ; let's use Typescript to describe it :

```typescript
interface Person { // 👈  Target with clean types
    firstName: string
    numberOfHobbies: number
    birthDate: Date
    hobbies: string[]
}
```

How to parse with `JSON.parse()` the incoming data to the expected target ?

Just use Jsonizer's revivers to fix anything :

```bash
npm install @badcafe/jsonizer
```
```typescript
import { Jsonizer } from '@badcafe/jsonizer';
```

Describe the source shape as it is :

```typescript
interface PersonDTO { // 👈  Source with bad types
    firstName: string
    numberOfHobbies: string
    birthDate: string
    hobbies: string
}
```

> DTO stands for [Data Transfer Object](https://en.wikipedia.org/wiki/Data_transfer_object)

Then define the mappings for every field to fix ; in Jsonizer, a mapping is just a plain object that contains an entry for each field to map :

```typescript
                                   //  Target  Source
                                   //    👇       👇
const personReviver = Jsonizer.reviver<Person, PersonDTO>({
    numberOfHobbies: {
        //  👇 fix the type
        '.': n => parseInt(n)
    },
    birthDate: Date,
    hobbies: {
        //  👇 split CSV to array
        '.': csv => csv.split(',')
    }
})
```

Each entry is bound to its reviver that can be a class such as `Date`, or a nested mapping for hierarchic structures, or nothing to left the field as-is, such as for the `firstName`.

The special mapping entry `'.'` stands for the familiar 'self' reference ; it is bound to a function that returns the expected data. Jsonizer also supply the `'*'` mapping that stands for the familiar 'any' item (object field or array item) and it is also possible to use Regexp matchers and range matchers for arrays.

However, there is a mapping that doesn't work ; let's try it with the incoming data :

```javascript
new Date('21/10/1998')
// Invalid Date
```

Since the `birthDay` input field is such a formatted date, we have to rewrite the mapping for it :

```typescript
                                   //  Target  Source
                                   //    👇       👇
const personReviver = Jsonizer.reviver<Person, PersonDTO>({
    numberOfHobbies: {
        //  👇 fix the type
        '.': n => parseInt(n)
    },
    birthDate: {
        //  👇 fix the Date
        '.': date => {
            const [day, month, year] = date.split('/')
                .map(part => parseInt(part));
            return new Date(Date.UTC(year, month - 1, day));
        }
    },
    hobbies: {
        //  👇 split CSV to array
        '.': csv => csv.split(',')
    }
})
```

> Notes:
> 1. don't use `new Date(year, month - 1, day)` because it may shift due to the local time zone.
> 1. Javascript use month indexes, so we use `month - 1`.

Finally, parse the data :

```typescript
const personJson = await read('person.json');
const person = JSON.parse(personJson, personReviver);
```

Since this exemple is somewhat simple with a flat structure, you might be tempted to write your own reviver function, but for nested structures it will become harder than you think.

With [Jsonizer](https://badcafe.github.io/jsonizer), you'll be able to define mappings for classes, plain objects, nested structures... and more.

> See also :
> - [Reviving JSON classes made easy](https://dev.to/ppoulard/reviving-json-classes-made-easy-58in)