The toJSON() method in JavaScript
toJSON() is a special method that JavaScript calls automatically when an object is serialized through JSON.stringify(). It lets you control exactly what ends up in the JSON string: if an object has toJSON(), what gets serialized is the result of calling that method, not the object itself.
Theory
TL;DR
JSON.stringify(obj)first checks whether the object has atoJSON()method.- If it does, the result of that call is serialized instead of the object.
- It works recursively: a nested object's
toJSON()is called too. - You can declare it in your own classes, handy for a clean data shape.
- It may return any value: an object, a string, a number,
null. Datehas its owntoJSON(), which is why it automatically becomes an ISO string.- It has no effect on
JSON.parse()at all.
Quick example
const user = {
name: 'Alice',
age: 25,
password: 'secret',
toJSON() {
// pick what ends up in the JSON
return { name: this.name, age: this.age };
}
};
console.log(JSON.stringify(user));
// '{"name":"Alice","age":25}'password did not make it into the JSON, because toJSON() returns only name and age.
How it works under the hood
When you call:
JSON.stringify(obj)JavaScript does the following:
-
It checks whether
objhas atoJSON()method. If it does, it calls it:javascriptconst value = obj.toJSON(); -
It serializes the result of that call, not the object itself.
In other words, toJSON() is an extension point of the format itself: a type decides on its own what it looks like in JSON.
Recursion in nested objects
toJSON() fires for every nested object, not only for the root one.
const user = {
name: 'Alice',
stats: {
score: 42,
toJSON() {
return 'Top Secret'; // replace the object with a string
}
}
};
console.log(JSON.stringify(user));
// '{"name":"Alice","stats":"Top Secret"}'The toJSON() method inside stats was called automatically and replaced the nested object.
toJSON() in classes and built-in types
The method is convenient in your own classes, so instances serialize nicely:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
toJSON() {
return {
user: this.name,
age: this.age
};
}
}
const alice = new User('Alice', 25);
console.log(JSON.stringify(alice));
// '{"user":"Alice","age":25}'This is how you define a clean representation of the data, for example for sending it to a server.
Built-in types behave differently:
| Type | What toJSON() does | Example |
|---|---|---|
Date | Returns an ISO string | new Date().toJSON() gives "2025-10-14T17:00:00.000Z" |
Map / Set | Not serialized (by default) | JSON.stringify(new Map()) gives {} |
BigInt | Not supported | Error: TypeError: Do not know how to serialize a BigInt |
What it may return and how it combines with replacer
You may return any value: a string, a number, even null.
const product = {
name: 'T-shirt',
price: 1500,
toJSON() {
return `${this.name}: ${this.price} UAH`;
}
};
console.log(JSON.stringify(product));
// '"T-shirt: 1500 UAH"'The returned string becomes the entire serialization result.
If an object has both toJSON() and a replacer passed in, toJSON() runs first and its result is what reaches the replacer:
const obj = {
name: 'Alice',
toJSON() {
return { custom: true };
}
};
console.log(JSON.stringify(obj, (key, value) => {
if (key === 'custom') return 'ok';
return value;
}));
// '{"custom":"ok"}'Summary
| Question | Answer |
|---|---|
| What it does | Defines what will be serialized by JSON.stringify() |
| Where it is called | Automatically during JSON.stringify() |
| What it returns | Any value (object, string, number and so on) |
| Does it work recursively | Yes, for nested objects |
| Can it be used in classes | Yes |
Does it affect JSON.parse() | No, it only affects serialization |
Common mistakes
- Expecting
JSON.parse()to give back a class instance.toJSON()is one way: restoring the type needs your own constructor, a factory, or areviverinJSON.parse(). - Declaring
toJSONas an arrow function class field. Thenthisdoes not point where you expect; use a regular method. - Returning the object itself (
return this) fromtoJSON(). For the root call that is either infinite recursion or the same result with no benefit. - Thinking
toJSON()encrypts or protects data. It only shapes the representation; a hidden field is still readable straight off the object. - Forgetting that the method also fires for nested values. A single
toJSON()deep in the tree can silently change the whole payload. - Counting on
toJSON()forMap,SetorBigInt. They do not have one (andBigIntthrows aTypeError), so you have to write the conversion yourself.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.