Understanding JSON.stringify
Custom implementation of JSON.stringify
Hello, beautiful people on the internet.
The JSON.stringify method in JavaScript transforms JavaScript objects or values into a JSON string. This string can be sent over the network, saved to storage, or logged for debugging. For example:
const obj = { name: "Deep", age: 20 };
const jsonString = JSON.stringify(obj);
console.log(jsonString); // '{"name":"Deep","age":20}'How JSON.stringify Works
The built-in JSON.stringify method is quite flexible and handles various data types differently. Let’s explore how it works for different types:
Primitive Types (Boolean, Number, String):
- These values are directly converted into their string representation.
Arrays:
- Arrays are serialized into JSON arrays, with each element processed individually.
Objects:
- JavaScript objects are serialized as JSON objects, and their enumerable properties are processed.
Date Objects:
Dateobjects are serialized into a string in ISO 8601 format.
Special Cases:
undefined,function, andsymbolare ignored.- Circular references (where an object references itself) throw an error.
- BigInt values cannot be serialized.
Custom Implementation of JSON.stringify
To handle all edge cases and better understand the serialization process, let’s implement a custom version of JSONStringify that addresses special cases like circular references, custom toJSON methods, and non-enumerable properties.
Primitive Types: Boolean, Number, and String
Implementation
Primitive types like boolean, number, and string are directly converted to their string representation.
// Handling booleans, numbers, and strings
if (typeof(value) === 'boolean' || typeof(value) === 'number') {
return value.toString();
}
if (typeof(value) === 'string') {
return `"${value}"`;
}Example Code
console.log(JSONStringify(true)); // 'true'
console.log(JSONStringify(2)); // '2'
console.log(JSONStringify("foo")); // '"foo"'null, NaN, and Infinity
Implementation
Values like null, NaN, and Infinity are special cases. For serialization, NaN and Infinity are treated asnull, as they are not valid JSON values.
null is the most appropriate representation for NaN and Infinity in JSON, as these values are not supported directly.
Get Deep Shah’s stories in your inbox
Join Medium for free to get updates from this writer.
By replacing them withnull, we avoid breaking the JSON structure or causing errors when attempting to parse the JSON.
// Handling null, NaN, and Infinity
const nullableValue = (val) => {
return (
Number.isNaN(val) ||
val === null ||
(typeof(val) === 'number' && !Number.isFinite(val))
);
};
if (nullableValue(value)) return 'null';Example Code
console.log(JSONStringify(NaN)); // 'null'
console.log(JSONStringify(null)); // 'null'
console.log(JSONStringify(Infinity)); // 'null'undefined, function, and Symbol
Implementation
In objects, we check for avoidable values such as undefined, function, and symbol. These values are ignored in the final JSON output because JSON doesn't support them:
undefinedis not a valid JSON value and should be omitted.functionvalues are not serializable, and their inclusion in JSON doesn't make sense, so they are ignored.symbolvalues are not supported by the JSON format and should be excluded.
// Handling function, undefined, and symbol
const ignorableValue = (val) => {
return (
typeof(val) === 'function' ||
val === undefined ||
typeof(val) === 'symbol'
)
}
if (ignorableValue(value)) return 'null';Example Code
console.log(JSONStringify(() => {})); // prints nothing
console.log(JSONStringify(undefined)); // prints nothing
console.log(JSONStringify(Symbol())); // prints nothingDate Object
Implementation
Date objects are serialized into strings using the toISOString() method, which returns a standardized date-time string.
// Handling Date objects
if (value instanceof Date) {
return value.toISOString();
}Example Code
console.log(JSONStringify(new Date('2023-06-01'))); // '"2023-06-01T00:00:00.000Z"'Array
Implementation
Arrays are serialized as JSON arrays. Each element is processed recursively to handle nested structures.
// Handling Arrays
if (Array.isArray(value)) {
let result = [];
value.forEach(ele => {
// If the element is a nullable value (NaN, null, Infinity) or an ignorable value (undefined, function, symbol)
if (nullableValue(ele) || ignorableValue(ele)) {
// Push 'null' to the result because these values are either unrepresentable in JSON or should be ignored
result.push('null');
} else {
result.push(JSONStringify(ele, visited));
}
});
return `[${result.join(',')}]`;
}Example Code
console.log(JSONStringify([1, "false", false])); // '[1,"false",false]'
console.log(JSONStringify([NaN, null, Infinity])); // '[null,null,null]'
console.log(JSONStringify([1, {x: 2}, [3, 4]])); // '[1,{"x":2},[3,4]]'Object
Implementation
For objects, we serialize the enumerable properties into key-value pairs. We also handle cases for toJSON methods and circular references.
// Handling regular objects
if (typeof(value) === 'object') {
const keys = Object.keys(value);
const result = [];
for (let key of keys) {
const val = value[key];
// If the value is not ignorable (e.g., function, undefined, symbol), proceed with stringifying it
if (!ignorableValue(val)) {
result.push(`"${key}": ${JSONStringify(val, visited)}`);
}
}
return `{${result.join(',')}}`;
}Example Code
console.log(JSONStringify({ x: 5, y: 6 })); // '{"x":5,"y":6}'
console.log(JSONStringify({ name: "Deep", age: 20 })); // '{"name":"Deep","age":20}'Handling Circular References
Implementation
Circular references occur when an object refers to itself, causing an infinite loop. To prevent this, we track visited objects and throw an error if a circular reference is detected.
// Handling circular references
if (typeof value === 'object' && value !== null) {
if (visited.has(value)) {
throw new TypeError('Converting circular structure to JSON');
}
visited.set(value, true);
}Example Code
const person = { name: 'Deep' };
person.self = person; // Circular reference
try {
console.log(JSONStringify(person)); // Throws TypeError: Converting circular structure to JSON
} catch (error) {
console.log(error.message);
}Special Objects: Map, Set, WeakMap, WeakSet
Implementation
For specialized objects like Map, Set, WeakMap, and WeakSet, our custom implementation returns empty objects because these structures don't have a straightforward serialization format.
// Handling special objects like Map, Set, WeakMap, WeakSet
if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
return '{}';
}Example Code
console.log(JSONStringify(new Set([1]))); // '{}'
console.log(JSONStringify(new Map([[1, 2]]))); // '{}'BigInt
Implementation
BigInt values are not serializable in JSON, so our implementation throws an error if a BigInt is encountered.
// Handling BigInt values
if (typeof(value) === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt');
}Example Code
try {
console.log(JSONStringify({ x: 2n })); // Throws TypeError: Do not know how to serialize a BigInt
} catch (error) {
console.log(error.message);
}Handling toJSON Method
Implementation
If an object has a toJSON method, we call it before serializing the object. This allows the object to control how it should be serialized.
// Handling toJSON method
if (typeof value === 'object' && typeof value.toJSON === 'function') {
return JSONStringify(value.toJSON(), visited);
}Example Code
const person = {
name: 'Deep',
age: 20,
toJSON: function() {
return { visibleData: 'Not hidden' };
}
};
console.log(JSONStringify(person)); // '{"visibleData":"Not hidden"}'Entire Implementation
const JSONStringify = (value, visited = new Map()) => {
const nullableValue = (val) => {
return (
Number.isNaN(val) ||
val === null ||
(typeof(val) === 'number' && !Number.isFinite(val))
)
}
const ignorableValue = (val) => {
return (
typeof(val) === 'function' ||
val === undefined ||
typeof(val) === 'symbol'
)
}
if(nullableValue(value)) return 'null';
if(ignorableValue(value)) return undefined;
// boolean, numbers
if(
typeof(value) === 'boolean' ||
value instanceof Boolean ||
typeof(value) === 'number' ||
value instanceof Number
)
return value.toString();
// string
if(typeof(value) === 'string' || value instanceof String)
return `"${value}"`
// date
if(value instanceof Date){
return value.toISOString();
}
// Check for circular reference
if (typeof value === 'object' && value !== null) {
// console.log(value, visited)
if (visited.has(value)) {
throw new TypeError('Converting circular structure to JSON');
}
visited.set(value, true);
}
// toJSON method
if (
typeof value === 'object' &&
typeof value.toJSON === 'function'
) return JSONStringify(value.toJSON(), visited);
// array
if(Array.isArray(value)){
let result = [];
value.forEach(ele => {
if(nullableValue(ele) || ignorableValue(ele)){
result.push('null');
} else {
result.push(JSONStringify(ele, visited))
}
})
return `[${result.join(',')}]`;
}
// Map, Set, WeakMap, WeakSet
if(
value instanceof Map ||
value instanceof Set ||
value instanceof WeakMap ||
value instanceof WeakSet
) return '{}'
// BigInt
if(typeof(value) === 'bigint'){
throw new TypeError('Do not know how to serialize a BigInt')
}
// object, typed arrays
if(typeof(value) === 'object'){
const keys = Object.keys(value);
const result = [];
for(let key of keys){
const val = value[key];
if(!ignorableValue(val)){
result.push(`"${key}": ${JSONStringify(val, visited)}`)
}
}
return `{${result.join(',')}}`
}
}
JSONStringify({}); // '{}'
JSONStringify(true); // 'true'
JSONStringify(2); // '2'
JSONStringify("foo"); // '"foo"'
JSONStringify([1, "false", false]); // '[1,"false",false]'
JSONStringify([NaN, null, Infinity]); // '[null,null,null]'
JSONStringify({ x: 5 }); // '{"x":5}'
JSONStringify(new Date(1906, 0, 2, 15, 4, 5));
// '"1906-01-02T15:04:05.000Z"'
JSONStringify({ x: 5, y: 6 });
// '{"x":5,"y":6}'
JSONStringify([new Number(3), new String("false"), new Boolean(false)]);
// '[3,"false",false]'
// String-keyed array elements are not enumerable and make no sense in JSON
const a = ["foo", "bar"];
a["baz"] = "quux"; // a: [ 0: 'foo', 1: 'bar', baz: 'quux' ]
JSONStringify(a);
// '["foo","bar"]'
JSONStringify({ x: [10, undefined, function () {}, Symbol("")] });
// '{"x":[10,null,null,null]}'
// Standard data structures
JSONStringify([
new Set([1]),
new Map([[1, 2]]),
new WeakSet([{ a: 1 }]),
new WeakMap([[{ a: 1 }, 2]]),
]);
// '[{},{},{},{}]'
// TypedArray
JSONStringify([new Int8Array([1]), new Int16Array([1]), new Int32Array([1])]); // '[{"0":1},{"0":1},{"0":1}]'
JSONStringify([
new Uint8Array([1]),
new Uint8ClampedArray([1]),
new Uint16Array([1]),
new Uint32Array([1]),
]); // '[{"0":1},{"0":1},{"0":1},{"0":1}]'
JSONStringify([new Float32Array([1]), new Float64Array([1])]); // '[{"0":1},{"0":1}]'
// Circular reference
const person = {
name: 'Deep',
age: 20
}
person.age = person;
JSONStringify(person) // TypeError: Converting circular structure to JSON
// Date object (which has a built-in toJSON method)
console.log(JSONStringify(new Date('2023-06-01'))); // '"2023-06-01T00:00:00.000Z"'
// toJSON()
JSONStringify({
x: 5,
y: 6,
toJSON() {
return this.x + this.y;
},
}); // '11'
// Symbols:
JSONStringify({ x: undefined, y: Object, z: Symbol("") }); // '{}'
JSONStringify({ [Symbol("foo")]: "foo" }); // '{}'
// Non-enumerable properties:
JSONStringify(
Object.create(null, {
x: { value: "x", enumerable: false },
y: { value: "y", enumerable: true },
}),
);
// '{"y":"y"}'
// BigInt values throw
JSONStringify({ x: 2n }); // TypeError: BigInt value can't be serialized in JSONI’m always looking to improve and would love to hear your thoughts! If you have any suggestions, feedback, or topics you’d like me to cover, please feel free to share them in the comments below. Your insights are greatly appreciated! 🙂
Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast
- Check out CoFeed, the smart way to stay up-to-date with the latest in tech 🧪
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com









