DEV Community

Cover image for How to fix a bad JSON structure
Philippe Poulard
Philippe Poulard

Posted on • Edited on

How to fix a bad JSON structure

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

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

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

interface Person { // 👈  Target with clean types
    firstName: string
    numberOfHobbies: number
    birthDate: Date
    hobbies: string[]
}
Enter fullscreen mode Exit fullscreen mode

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

Just use Jsonizer's revivers to fix anything :

npm install @badcafe/jsonizer
Enter fullscreen mode Exit fullscreen mode
import { Jsonizer } from '@badcafe/jsonizer';
Enter fullscreen mode Exit fullscreen mode

Describe the source shape as it is :

interface PersonDTO { // 👈  Source with bad types
    firstName: string
    numberOfHobbies: string
    birthDate: string
    hobbies: string
}
Enter fullscreen mode Exit fullscreen mode

DTO stands for 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 :

                                   //  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(',')
    }
})
Enter fullscreen mode Exit fullscreen mode

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 :

new Date('21/10/1998')
// Invalid Date
Enter fullscreen mode Exit fullscreen mode

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

                                   //  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(',')
    }
})
Enter fullscreen mode Exit fullscreen mode

Notes:

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

Finally, parse the data :

const personJson = await read('person.json');
const person = JSON.parse(personJson, personReviver);
Enter fullscreen mode Exit fullscreen mode

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, you'll be able to define mappings for classes, plain objects, nested structures... and more.

See also :

Top comments (0)