Skip to main content

2 posts tagged with "json"

View All Tags

· 2 min read
Peter Johnson

Object keys in a Javascript maintain insertion order.

For some crypto tasks, such as hashing and signing, it's desirable to have a normalized view of the data.

Fortunately it's easy enough to recursively sort object keys using just a few lines of code:

function replacer (key, value) {
switch (Object.prototype.toString.call(value)) {

// sort object keys lexicographically
case '[object Object]':
return Object.fromEntries(Object.entries(value).sort())

default:
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, replacer, 2)

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

Additional types

It's also possible to extend this method to other types:

function replacer (key, value) {
switch (Object.prototype.toString.call(value)) {

// sort object keys lexicographically
case '[object Object]':
return Object.fromEntries(Object.entries(value).sort())

// Unicode Normalization Form KC
case '[object String]':
return value.normalize('NFKC')

// round floats to 7 decimal places of precision
case '[object Number]':
return value % 1 === 0
? value
: Math.round(parseFloat(value) * 1e7) / 1e7

default:
return value
}
}

· 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