toJSON()

ES5+

toJSON() is a serialization protocol: when JSON.stringify() encounters any object exposing this method, the method's return value is serialized in place of the object itself. Date implements it natively (producing ISO strings), and defining it on your own classes gives them a canonical, self-describing wire format.

Syntax

object.toJSON()

Return Value

any

The value to be serialized

Examples

JavaScript
const user = {
  name: 'John',
  password: 'secret',
  toJSON() {
    return { name: this.name };
  }
};
console.log(JSON.stringify(user));
Output:
// '{"name":"John"}'

📌 When to Use

Define toJSON() when a type — not a call site — should own its serialized shape. It is the right tool for domain classes that appear in many payloads: a User that must always exclude password hashes and internal flags, a Money type that serializes as { amount, currency } regardless of who stringifies it, a tree node that flattens parent references to IDs so cycles never reach the serializer. Because JSON.stringify() invokes it automatically, every serialization site in the codebase — logging, caching, API bodies, localStorage — emits the same canonical form without remembering any options, which is precisely its advantage over replacer functions: replacers apply per call, toJSON() applies per type. Use it also to bridge non-JSON types (expose a Set field as an array, a Map as entries) and to version wire formats by including a schema tag in the output. Skip it when serialization legitimately varies by context — a public API response versus an internal audit log — where per-call replacers or explicit DTO mapping functions express the variation better. Combining both works too: toJSON() defines the base shape, and a replacer applies call-specific redaction on top of it.

⚠️ Common Mistakes

Not knowing the precedence order - toJSON() runs before any replacer sees the value, so the replacer receives the converted output, not the original object. Checks like value instanceof MyClass inside a replacer silently stop matching the moment the class gains a toJSON() method.

Losing the class on the round trip and blaming parse - toJSON() output is plain data, and JSON.parse() returns plain objects; nothing automatically calls a constructor. Pair each toJSON() with a static fromJSON() (or a reviver) if instances with methods must survive persistence.

Hiding expensive work or side effects inside it - a toJSON() that computes reports or touches external state runs on every stringify, including debug logging you forgot about, making serialization slow or observably impure. Keep it a cheap projection of existing fields.

Defining toJSON() that returns the object itself, causing infinite recursion.

Forgetting that toJSON() return value is also stringified, so return objects, not strings.

✅ Best Practices

Treat toJSON() and a static fromJSON() as a matched pair - class User { toJSON() { ... } static fromJSON(data) { return new User(...); } } - so persistence round trips restore behavior-bearing instances instead of leaving bare data objects scattered through the app.

Whitelist, never blacklist, inside toJSON() - return an object listing exactly the fields to expose. New sensitive fields added to the class later stay private by default, whereas a delete-the-secrets approach leaks every field someone forgets to remove.

Remember it receives a key argument - toJSON(key) is passed the property name or array index under which the object is being serialized, occasionally useful for context-sensitive output; but relying on it couples the type to its container, so prefer explicit DTO methods when context truly changes the shape.

Use toJSON() to exclude computed properties, methods, and circular references from serialization.

Return a plain object with only the data you want serialized.

⚡ Performance Notes

toJSON() executes once for every instance encountered during a stringify, so its cost scales with how many such objects a payload contains — a list of 10,000 records calls it 10,000 times. A simple object-literal projection is cheap and usually invisible next to the serializer's own traversal, but each call does allocate a fresh intermediate object, doubling short-lived allocations for large arrays of instances; keep the method allocation-light and free of loops over unrelated data. Never fetch, compute derived analytics, or touch the DOM inside it. If the same immutable instance is serialized extremely often, caching its toJSON() result in a private field and invalidating on mutation trades a little memory for real speed. Native Date.prototype.toJSON is highly optimized; custom implementations on hot types deserve a quick profile before shipping into logging paths that run on every request.

🌍 Real World Example

Custom User Class Serialization

This User class shows toJSON() as an API-shaping tool. The constructor holds sensitive internals — a password and an underscore-prefixed creation timestamp — while toJSON() returns a whitelisted projection: public identity fields, the computed isAdmin getter materialized as a plain boolean, and the timestamp normalized to an ISO string. Every JSON.stringify(user) anywhere in the application now emits exactly this safe shape, with the password structurally incapable of leaking. The pattern is the standard way ORMs and domain models keep secrets out of HTTP responses.

class User {
  constructor(id, name, email, password) {
    this.id = id;
    this.name = name;
    this.email = email;
    this._password = password; // Private
    this._createdAt = new Date();
  }

  get isAdmin() {
    return this.email.endsWith('@admin.com');
  }

  toJSON() {
    return {
      id: this.id,
      name: this.name,
      email: this.email,
      isAdmin: this.isAdmin,
      createdAt: this._createdAt.toISOString()
      // password is excluded!
    };
  }
}

const user = new User(1, 'John', 'john@admin.com', 'secret123');
console.log(JSON.stringify(user, null, 2));
// { "id": 1, "name": "John", "email": "john@admin.com", "isAdmin": true, "createdAt": "2024-..." }

Related Methods