Skip to main content

JSON.stringify() replacer

ยท 2 min read
Peter Johnson

One little used option of JSON.stringify(value, replacer, space) is the second argument replacer, which i've never seen set to anything except null.

As it turns out, that's for a very good reason. The replacer argument can either be provided as an Array or a Function, and both implementations have poor support for nested JSON.

warning

Using either syntax results in considerably more bug than feature.

Saving Graceโ€‹

However! in the footnotes of the documentation we find a clue to saving this feature from the ๐Ÿ—‘๏ธ of history:

The object in which the key was found is provided as the replacer's this context.

In other words, it calls (key, value) for the current JSON element, but also binds this to the parent element ๐Ÿค”

Get the JSON Path within replacerโ€‹

With this in mind we can create a decorator function which tracks the JSON Path for each key as we traverse the object hierarchy:

function replacerWithPath (fn) {
const paths = new Map()
return function (key, value) {
let path = paths.get(this) || '$'
if (key) path += Array.isArray(this) ? `[${key}]` : `.${key}`
const v = fn(key, value, path)
if (v === Object(v)) paths.set(v, path)
return v
}
}

Now our replacer function will receive a third argument with the full path:

function replacer (key, value, path) {
console.log(path, key)
return value
}

Exampleโ€‹

You can use it as you normally would with JSON.stringify():

const example = {
"isbn": "123-456-222",
"author": {
"lastname": "Doe",
"firstname": "Jane"
},
"editor": {
"lastname": "Smith",
"firstname": "Jane"
},
"title": "The Ultimate Database Study Guide",
"category": [
"Non-Fiction",
"Technology"
]
}

> JSON.stringify(example, replacerWithPath(replacer), 2)

$
$.isbn isbn
$.author author
$.author.lastname lastname
$.author.firstname firstname
$.editor editor
$.editor.lastname lastname
$.editor.firstname firstname
$.title title
$.category category
$.category[0] 0
$.category[1] 1