I am trying to learn recursion in Javascript, so I figured I'd rewrite the native JSON.stringify
function using recursion as a challenge to myself. I almost got my code to work:
var my_stringify = function(obj){
value = obj[ Object.keys(obj)[0] ];
index = Object.keys(obj)[0];
delete obj[ Object.keys(obj)[0] ];
// The value is just a simple string, not a nested object
if (typeof value === 'string'){
if (Object.keys(obj).length !== 0){
// Continue recursion ..
return '"' + index + '":"' + value + '",' + my_stringify(obj);
}
// This would be the base case with a string at the end. Stop recursion.
return '"' + index + '":"' + value + '"}';
}
// The value is actually a nested object
else{
if (Object.keys(obj).length !== 0){
// Continue recursion ..
return '"' + index + '":{' + my_stringify(value) + ',' + my_stringify(obj);
}
// This is the base case with a nested object at the end. Stringify it and end recursion.
return '"' + index + '":{' + my_stringify(value) + '}';
}
}
Except for the fact that the first {
in my answer is missing, and I can't figure out how to fix this bug.
E.g. my_stringify({foo: 'bar'})
returns "foo":"bar"}
instead of {"foo":"bar"}
.
Also, I'm aware I'm completely destroying the original object, is there any way to send over to recursion a reduced version of the original object without deleting anything (something like obj.slice(1)
)?
Any advice will be greatly appreciated !
2024, type safe
This updated version is type safe and handles function
, symbol
, and bigint
types -
function stringifyJSON(t: unknown): undefined | string {
if (t === undefined) return undefined
else if (t === null) return 'null'
else if (typeof t == 'bigint') throw TypeError('stringifyJSON cannot serialize BigInt')
else if (typeof t == 'number') return String(t)
else if (typeof t == 'boolean') return t ? 'true' : 'false'
else if (typeof t == 'string') return '"' + t.replace(/"/g, '\\"') + '"'
else if (typeof t == 'object') return Array.isArray(t)
? '[' + Array.from(t, v => stringifyJSON(v) ?? 'null').join(',') + ']'
: '{' + Object.entries(t)
.map(([k,v]) => [stringifyJSON(k), stringifyJSON(v)])
.filter(([k,v]) => v !== undefined)
.map(entry => entry.join(':'))
.join(',') + '}'
else return undefined
}
2024, circular reference check
This updated version throws an error when a circular object reference is used -
function stringifyJSON(t: unknown, seen: Set<unknown> = new Set()): undefined | string {
if (seen.has(t)) throw TypeError('stringifyJSON cannot serialize cyclic structures')
else if (t === undefined) return undefined
else if (t === null) return 'null'
else if (typeof t == 'bigint') throw TypeError('stringifyJSON cannot serialize BigInt')
else if (typeof t == 'number') return String(t)
else if (typeof t == 'boolean') return t ? 'true' : 'false'
else if (typeof t == 'string') return '"' + t.replace(/"/g, '\\"') + '"'
else if (typeof t == 'object') {
const nextSeen = new Set(seen).add(t)
return Array.isArray(t)
? '[' + Array.from(t, v => stringifyJSON(v, nextSeen) ?? 'null').join(',') + ']'
: '{' + Object.entries(t)
.map(([k,v]) => [stringifyJSON(k, nextSeen), stringifyJSON(v, nextSeen)])
.filter(([k,v]) => v !== undefined)
.map(entry => entry.join(':'))
.join(',') + '}'
}
else return undefined
}
2017, new answer to an old question
There's some painfully bad answers here that fail under even the simplest examples. This answer aims to answer the question exhaustively and demonstrate how an approach like this scales even when handling a wide variety of data types and ...
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({[undefined]: 1})
returns '{ "undefined": 1 }'
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({[undefined]: 1}) // { undefined: 1 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]