Do you guys have any ideas on what I might be able to change to make this work for nested examples?
Sure, but it's going to scrap you entire function, so I hope you don't mind. I'll provide a bullet-list of points why this approach is essential and yours is essentially flawed from the get-go :(
Corner cases
This function does a simple case analysis on a non-null data's constructor
property and encodes accordingly. It manages to cover a lot of corner cases that you're unlikely to consider, such as
JSON.stringify(undefined)
returns undefined
JSON.stringify(null)
returns 'null'
JSON.stringify(true)
returns 'true'
JSON.stringify([1,2,undefined,4])
returns '[1,2,null,4]'
JSON.stringify({a: undefined, b: 2})
returns '{ "b": 2 }'
JSON.stringify({a: /foo/})
returns { "a": {} }
So to verify that our stringifyJSON
function actually works properly, I'm not going to test the output of it directly. Instead, I'm going to write a little test
method that ensures the JSON.parse
of our encoded JSON actually returns our original input value
// we really only care that JSON.parse can work with our result
// the output value should match the input value
// if it doesn't, we did something wrong in our stringifier
const test = data => {
return console.log(JSON.parse(stringifyJSON(data)))
}
test([1,2,3]) // should return [1,2,3]
test({a:[1,2,3]}) // should return {a:[1,2,3]}
Disclaimer: it should be obvious that the code I'm about to share is not meant to be used as an actual replacement for JSON.stringify
– there's countless corner cases we probably didn't address. Instead, this code is shared to provide a demonstration for how we could go about such a task. Additional corner cases could easily be added to this function.
Runnable demo
Without further ado, here is stringifyJSON
in a runnable demo that verifies excellent compatibility for several common cases
const stringifyJSON = data => {
if (data === undefined)
return undefined
else if (data === null)
return 'null'
else if (data.constructor === String)
return '"' + data.replace(/"/g, '\\"') + '"'
else if (data.constructor === Number)
return String(data)
else if (data.constructor === Boolean)
return data ? 'true' : 'false'
else if (data.constructor === Array)
return '[ ' + data.reduce((acc, v) => {
if (v === undefined)
return [...acc, 'null']
else
return [...acc, stringifyJSON(v)]
}, []).join(', ') + ' ]'
else if (data.constructor === Object)
return '{ ' + Object.keys(data).reduce((acc, k) => {
if (data[k] === undefined)
return acc
else
return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])]
}, []).join(', ') + ' }'
else
return '{}'
}
// round-trip test and log to console
const test = data => {
return console.log(JSON.parse(stringifyJSON(data)))
}
test(null) // null
test('he said "hello"') // 'he said "hello"'
test(5) // 5
test([1,2,true,false]) // [ 1, 2, true, false ]
test({a:1, b:2}) // { a: 1, b: 2 }
test([{a:1},{b:2},{c:3}]) // [ { a: 1 }, { b: 2 }, { c: 3 } ]
test({a:[1,2,3], c:[4,5,6]}) // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] }
test({a:undefined, b:2}) // { b: 2 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]
"So why is this better?"
- this works for more than just Array types – we can stringify Strings, Numbers, Arrays, Objects, Array of Numbers, Arrays of Objects, Objects containing Arrays of Strings, even
null
s and undefined
s, and so on – you get the idea
- each case of our
stringifyJSON
object is like a little program that tells us exactly how to encode each type (eg String
, Number
, Array
, Object
etc)
- no whacked out
typeof
type checking – after we check for the undefined
and null
cases, we know we can try to read the constructor
property.
- no manual looping where we have to mentally keep track of counter variables, how/when to increment them
- no complex
if
conditions using &&
, ||
, !
, or checking things like x > y.length
etc
- no use of
obj[0]
or obj[i]
that stresses our brain out
- no assumptions about Arrays/Objects being empty – and without having to check the
length
property
- no other mutations for that matter – that means we don't have to think about some master return value
resarray
or what state it's in after push
calls happen at various stages in the program
Custom objects
JSON.stringify
allows us to set a toJSON
property on our custom objects so that when we stringify them, we will get the result we want.
const Foo = x => ({
toJSON: () => ({ type: 'Foo', value: x })
})
console.log(JSON.stringify(Foo(5)))
// {"type":"Foo","value":5}
We could easily add this kind of functionality to our code above – changes in bold
const stringifyJSON = data => {
if (data === undefined)
return undefined
else if (data === null)
return 'null'
else if (data.toJSON instanceof Function)
return stringifyJSON(data.toJSON())
...
else
return '{}'
}
test({toJSON: () => ({a:1, b:2})}) // { a: 1, b: 2 }