Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Understanding JSON.stringify

7 min readJan 2, 2025

--

Custom implementation of JSON.stringify

Press enter or click to view image in full size

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:

  • Date objects are serialized into a string in ISO 8601 format.

Special Cases:

  • undefined, function, and symbol are 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:

  • undefined is not a valid JSON value and should be omitted.
  • function values are not serializable, and their inclusion in JSON doesn't make sense, so they are ignored.
  • symbol values 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 nothing

Date 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 JSON

I’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:

--

Stackademic
Stackademic

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.