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)
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
);
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
);
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)
})
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
}
}
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
}
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);`
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);
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)
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
}
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)
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
}
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.
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)
great article! thanks for sharing! I'm not sure if I'm misunderstanding the example:
did you mean to use
key === 'city'
instead oftypeof value === 'city'
?