DEV Community

Cover image for Revive your JSON (examples with TDD and composition)
Davide de Paolis
Davide de Paolis

Posted on

Revive your JSON (examples with TDD and composition)

I have been working with JSON for years, but for some weird reason I never really paid too much attention at the second parameter or the JSON.parse method.

JSON.parse(text, reviver)
Enter fullscreen mode Exit fullscreen mode

reviver Optional
If a function, this prescribes how the value originally produced by parsing is transformed, before being returned.

Its simplicity blew my mind. It's incredible how after so many years there are so many things that I should know.

Imposter Syndrome vs Me --> 1:0

But it is one of the aspect of our job that is fascinating.
Never take anything for granted, and always read the docs!.

Anyway, the documentation is concise and clear, and there are plenty of posts explaining how to use it, so I won't bore you to death going into details, rather i'd like to add some tips about how to use it in a slightly more flexible and reusable way.

First of all, try coding in a TDD style.
and write tests first, in AAA Style.

This is also what i did to play around with Revivers.

  • Arrange you test, preparing a JSON file or stringifying an object and preparing the output that you expect.
  • Act, that means run your revivers while parsing the JSON object
  • Assert that your result matches the expectations.

But I don't have any revivers yet!, you might think. And yes, that's exactly the Test Driven Development approach.
Write tests before you even have the code you want to test.
And make your tests fail!

A quick intro about Revivers

As we said, a Reviver is a simple function that transform the value of a specific property or type in a JSON.

That means that you can write a reviver to double every number you have in the json (like shown in the docs), not very useful though.

JSON.parse('{"p": 5}', (key, value) =>
  typeof value === 'number'
    ? value * 2 // return value * 2 for numbers
    : value     // return everything else unchanged
);
Enter fullscreen mode Exit fullscreen mode

or you can change a specific property

JSON.parse('{"name": "joe", "city": "ny" }', (key, value) =>
  typeof value === 'city'
    ? value.toUpperCase()
    : value   // all other keys in the json that are not city will stay as they are
);
Enter fullscreen mode Exit fullscreen mode

the cool thing is that you can also:

  • remove a property/value
  • change the name of the property key.

Our first big reviver

If you need to manipulate your JSON, transforming multiple properties, your Reviver will have to specify the keys and implement all the tranformations for each of them, resulting in a quite big and complex (and boring ) reviving function.

Let's see how to write it starting with a test.

Imagine our JSON string to contain a name, a registration timestamp, an array and a password.

While parsing the JSON, we would like to Uppercase our user_name, change the timestamp from milliseconds to a formatted date string, change the order of the array and most importantly completely remove the password (who had the crazy idea of putting password in a json file anyway!!!).

So, we know our input data, and we know how it should look like after the parsing. Let's see how our test look like:

it("basic reviver with multiple features", () => {
    const json = JSON.stringify({
        name: "davide",
        registration: 1655642086006,
        sequence: [1, 2, 3, 4, 5],
        password : "qwerty1234"
    })

    const expectedRevived = {
        name: "DAVIDE",
        registration: '2022-06-19T12:34:46.006Z',
        sequence: [5, 4, 3, 2, 1]
    }
   const reviver = (key, value) => value // this reviver is doing NOTHING, since it returns the same value it receives, for each property
    const revived = JSON.parse(json, reviver) 
    expect(revived).toMatchObject(expectedRevived)
})

Enter fullscreen mode Exit fullscreen mode

In this examples I am using Typescript and Jest, but you can of course use simple javascript and any test framework like AVA or Mocha.

Of course this test will fail because since our reviver does nothing else than returning the original value, the result of our parse operation will not contain any transformation.

Now let's add the implementation of our reviver:

 const reviver = (key: string, value: any) => {
        switch (key) {
            case "sequence":
                return value.reverse()
            case "user_name" :
                return value.toUpperCase()
            case "registration":
                return new Date(value).toISOString()
            case "password":
                return
            default :
                return value
        }
    }
Enter fullscreen mode Exit fullscreen mode

A reviver methods accepts a Key and a Value - for each property /node of our JSON, our method will be invoked.

Since we need to apply multiple transformations on different properties, we have a big Switch and apply the uppercasing, reverting and so on returning the new value in case of a transformation, or the same value for all other keys.

What is very interesting to note is the Password case. By returning nothing (which means, return void, or null
) the property will be removed from the output!

If you run the test again you will have green results, the transformations are applied and the result matches the expectation you defined at the top.

This is already enough to understand how revivers work.
But, honestly I don't quite like the way the reviver is written.
We have a big method, with a switch case and all implementations altogether! Not really unit testable, nor reusable.

How can we get rid of the switch, split all those transformation into individual methods so that we can write individual unit tests for each of those ( of course string.toUpperCase() is very simple but maybe your specific needs are more complex - and you need to test many scenarios) and compose them accordingly in different JSON.parse circumstances?

Chaining Revivers together

Let's start by writing those individual methods:

const invertSequence = (key: string, value: any) => {
        if (key === "sequence") {
            // @ts-ignore
            // this.gnip = reversePlayerIndentifier(value)
            return value.reverse()
        } else {
            return value
        }
    }
    const stringifyRegistration = (key: string, value: any) =>  key === "registration" ? new Date(value).toISOString() : value

    const upperCaseUserName = (key: string, value: any) => 
   key === "user_name" ? value.toUpperCase() : value
    }
Enter fullscreen mode Exit fullscreen mode

How can we compose them together?
According to the docs reviver must be a function, how can we put all these methods together so that they are invoked after another with the current Key, and the value which is the result of the previous method?

We can use the Pipe approach, if you ever use pipe in the terminal, it is very similar, and specifically a simple implementation written by Eric Elliot in this awesome article about Composing Software and Functional Programming

`const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);`
Enter fullscreen mode Exit fullscreen mode

I modified his implementation to use typescript and specifically to accept not only the output from previous method but also passing down the key to every function in the array.

type ReviverFunc = (key: string, value: any) => any

const pipeRevivers = (revivers: ReviverFunc[ ]) => (key: string, value: any) => revivers.reduce((v, f) => f(key, v), value);

Enter fullscreen mode Exit fullscreen mode

Now that you have your individual revivers, and a method that pipes them together you can create an array of the revivers you need and pass the result of the piping method as a single reviver.

const revivers = pipeRevivers([
upperCaseUserName,
invertSequence, 
stringifyRegistration
])
const revived = JSON.parse(json, revivers)

Enter fullscreen mode Exit fullscreen mode

Do you agree that it is way more testable and readable, and you can swap or add revivers easily by just editing the array and not caring about the implementation?

Add even more flexibility with Partial Application

The above implementation is actually not really flexible though, because our revivers are bound to the the Key we check in the implementation.

 const upperCaseUserName = (key: string, value: any) => 
   key === "name" ? value.toUpperCase() : value
    }
Enter fullscreen mode Exit fullscreen mode

Of course we can reuse it in multiple places, but only if our json contains a name property, and only for that one, not if we want to uppercase the city, or the surname.

Partial Application is another interesting concept often confused with Currying which allow us to create different functions which have their own context, bound to the variable passed when they were created.

Easier coded than explained:

 const upperCase = (prop:string)=> (key: string, value: any) => 
   key === prop ? value.toUpperCase() : value
   }

 const revivers = pipeRevivers([
    upperCase("name"),
    upperCase("surname"),
    upperCase("city"),
])
const revived = JSON.parse(json, revivers)

Enter fullscreen mode Exit fullscreen mode

Our reviver is now a function receiving a key, which returns a function accepting a key,value and returns the transform value.

Our original method is the same but instead of checking the hardcoded property name, it relies on the variable in the first arrow function.

Pay attention at how the method is partially applied.
When we create the reviver like this upperCase("name") we end up with a function whose signature is similar to our original reviver - just accepting the key and value.

I really recommend another article about Partial Application and Currying - again from Eric Elliot.

Modifying the Keys

What if you need to add new properties to the JSON object or you want to rename the keys? Would a reviver work in such cases?
At a first glance, I thought that was not possible, remember that the signature of the reviver method is key,value and it returns only the value?

Well, thanks to the magic of javascript, it is indeed possible to change the key, and change the json object itself, but we need to give up arrow functions in this case. (or at least I wasn't quickly able to find a workaround for this case - feel free to post your suggestions in the comments).

Why? Because of this.

const renameUsername =  function (key: string, value: any) {
        if (key === "user_name" :
                // @ts-ignore
                this.userName = value. // I had to use ts-ignore here because Typescript was nagging that this has no userName property in its type definition.
                return null;
         }
         else {
               return value
    }

Enter fullscreen mode Exit fullscreen mode

With the usual function expression we have indeed access to this, and this is the object itself. So we can manipulate it as we like.

You can create new properties / keys and remove some, just add the new property assigning it to this, and return null to delete the current key.

From the docs

If the reviver function returns undefined (or returns no value, for example, if execution falls off the end of the function), the property is deleted from the object.

awesome

Recap

Even though the subject of this post is in itself rather simple, we discussed many different things:

  • Reviver method: prescribes how the value originally produced by parsing is transformed, before being returned.
  • TDD: Test Driven Development, write tests as/before you write your code, not after.
  • AAA Style: Act - Arrange - Assert (how to structure your tests)
  • Composing software: how to write methods which are (as functional as possible) reusable and composable, really like small building blocks
  • Pipe method: a method that allows to compose/chain an array of methods where the original input is edited and passed down to every function
  • Partial application vs Currying

I hope approaches and coding styles suggested in this post which will help you and make your code simpler, more readable, extendable and testable - if we want to stretch it a bit, more solid.


anyway, why Reviver?

Naming things is hard, we know, and among parser, replacer, formatter, tranformer, adapter I was wondering how we ended up calling revivers, revivers. Why do we need to bring back to life a JSON string?

After reading the discussion on StackOverflow, it makes perfectly sense.

I think rehydrate - used in other languages - is probably more straightforward, but the idea is similar:

you have an object - a "living" js object that you can manipulate and interact with, when JSON stringifying it, you dry it up, it become a plain dead string, when parsing it, it comes back as an object. It comes back to life! You revived it!

And if you are asking why the banner on top of the post - well, it is an homage to one of my favorite horror films when I was a kid, which always comes to my mind when I read the reviver parameter - Reanimator!

Top comments (1)

Collapse
 
onel0p3z profile image
Juan Lopez • Edited

great article! thanks for sharing! I'm not sure if I'm misunderstanding the example:



`JSON.parse('{"name": "joe", "city": "ny" }', (key, value) =>
  typeof value === 'city'
    ? value.toUpperCase()
    : value   // all other keys in the json that are not city will stay as they are
);`


Enter fullscreen mode Exit fullscreen mode

did you mean to use key === 'city' instead of typeof value === 'city'?