Date.prototype.toJSON()
Returns the date as an ISO 8601 UTC string, or null when the date is invalid.
toJSON()- Added in JavaScript ES5 (2009)
Return value
string | nullA string representing the date in the date time string format according to universal time — the same value as toISOString() — or null when the date is invalid.
Changelog
- PHP ES5Introduced together with JSON.stringify; invalid dates serialize to null rather than throwing, which is why a broken date silently becomes null in output JSON.
toJSON() exists so that JSON.stringify() knows what to do with a Date. You rarely call it yourself; JSON.stringify looks for a toJSON method on every value it visits and, if present, serializes that method's result instead of the object's own properties. Without it, a Date would stringify as {}, because its timestamp is stored in an internal slot, not an enumerable property.
Why dates come back as strings
JSON has no date type. Serialization is lossy in one direction only: JSON.parse() has no way to know that "2011-10-05T14:48:00.000Z" was ever a Date, so it hands you a string. Restore it with a reviver:
JSON.parse(text, (key, value) =>
key === "createdAt" ? new Date(value) : value
);Key-based revivers are safer than regex-sniffing every string value, which will happily convert an ID or a version number that happens to look like a date.
Invalid dates become null
This is the trap. toISOString() throws a RangeError on an invalid date, but toJSON() returns null — so a bad date is not an error, it is a null field in your payload that the server rejects hours later. Validate before serializing when the date came from user input or a parsed string.
Example
When to use it
- Sending a record containing timestamps to an API with JSON.stringify
- Writing a reviver that turns known date fields back into Date objects on parse
- Understanding why a Date read from a fetch response is a string, not a Date
Frequently asked questions
Why does JSON.parse give me a string instead of a Date?
Because JSON has no date type — JSON.stringify calls toJSON() to write an ISO string, and JSON.parse has no way to know that string was once a Date. Pass a reviver function as the second argument to JSON.parse to convert known date fields back.
Do I ever need to call toJSON directly?
Rarely — JSON.stringify calls it for you. Call it directly when you want the ISO string but want null rather than a RangeError for invalid dates, which is the only behavioural difference from toISOString().
Why is my date null in the JSON output?
The Date is invalid, usually because it was constructed from an unparseable string. toJSON() returns null for invalid dates instead of throwing, so the failure only surfaces downstream.
More examples
Reviving dates on parse
Only the named key is converted, so the id string "2024" survives untouched — a regex that matches anything date-shaped would have mangled it.
const text = '{"id":"2024","createdAt":"2011-10-05T14:48:00.000Z"}';
const record = JSON.parse(text, (key, value) =>
key === "createdAt" ? new Date(value) : value,
);
console.log(record.createdAt instanceof Date);
console.log(record.createdAt.getUTCFullYear());
console.log(typeof record.id);Overriding the serialized shape
Any object can define toJSON. Here the wrapper emits both a numeric timestamp and the ISO string, so consumers that cannot parse dates can still use the epoch value.
class Slot {
constructor(at) {
this.at = at;
}
toJSON() {
return { epoch: this.at.valueOf(), iso: this.at.toJSON() };
}
}
console.log(JSON.stringify(new Slot(new Date(Date.UTC(2024, 0, 1)))));Nothing survives without toJSON
Spreading a Date copies no own enumerable properties, so { ...d } stringifies as {}. The timestamp lives in an internal slot that only methods like toJSON and valueOf can reach.
const d = new Date(Date.UTC(2024, 0, 1));
console.log(JSON.stringify(d));
const stripped = Object.create(null);
stripped.value = d.valueOf();
console.log(JSON.stringify(stripped));
console.log(JSON.stringify({ d, plain: { ...d } }));
Discussion