JSON Date Format
JSON has no native date type. The JSON specification defines only strings, numbers, objects, arrays, booleans, and null — dates must be serialized as either an ISO 8601 string or a Unix timestamp number. This guide covers the standard format, how JavaScript's JSON.stringify and JSON.parse behave with dates, the reviver pattern for auto-converting date strings, Unix timestamp pitfalls, and more.
Paste a JSON payload with date fields into Jsonic to validate it instantly.
Open JSON FormatterWhy JSON has no date type: strings vs numbers
The JSON specification (RFC 8259) was designed to be a minimal, language-neutral format. Dates were deliberately excluded because different languages and platforms represent them differently — there is no universal "date" primitive that all environments agree on.
In practice, two conventions fill the gap:
// Option 1: ISO 8601 string — human-readable, unambiguous, sortable
{
"createdAt": "2024-06-15T10:30:00.000Z",
"updatedAt": "2024-06-15T14:45:22.123+05:30"
}
// Option 2: Unix timestamp number — compact, easy arithmetic
{
"createdAt": 1718447400, // seconds (most server-side APIs, POSIX)
"updatedAtMs": 1718447400000 // milliseconds (JavaScript Date.now())
}ISO 8601 strings are the dominant convention for REST APIs and JSON-LD schemas because they are self-documenting and unambiguous. Unix timestamps are common in high-frequency systems where byte count matters, but they require extra care because of the seconds-vs-milliseconds split (covered in detail below).
ISO 8601: the standard format with examples
ISO 8601 defines a family of date/time string patterns. The one universally used in JSON APIs is the combined date-time format with an explicit timezone offset:
// Full pattern: YYYY-MM-DDTHH:mm:ss.sssZ
"2024-06-15T10:30:00.000Z"
// ^^^^ ^^ ^^ ^^ ^^ ^^ ^^^
// year mo dd hr mn sc ms tz
// Timezone variants
"2024-06-15T10:30:00.000Z" // "Z" = UTC (zero offset)
"2024-06-15T10:30:00.000+00:00" // equivalent to Z
"2024-06-15T16:00:00.000+05:30" // IST (UTC+5:30)
"2024-06-15T03:30:00.000-07:00" // PDT (UTC-7)
// Date-only (no time component) — still valid ISO 8601
"2024-06-15"
// Milliseconds are optional
"2024-06-15T10:30:00Z" // no milliseconds — valid
"2024-06-15T10:30:00.000Z" // with milliseconds — preferred for JSON
// ⚠️ Missing timezone — ambiguous, avoid in APIs
"2024-06-15T10:30:00" // treated as local time by some parsers, UTC by othersThe "T" separator between date and time is required by strict ISO 8601 parsers. The trailing "Z" is shorthand for "+00:00" (UTC). Always include an explicit timezone suffix in API responses — omitting it creates ambiguity that breaks clients in different geographic regions.
Serializing dates with JSON.stringify
JavaScript's JSON.stringify() handles Date objects automatically by calling Date.prototype.toJSON(), which returns the date as a UTC ISO 8601 string:
// JSON.stringify automatically calls toJSON() on Date objects
const now = new Date('2024-06-15T10:30:00.000-07:00'); // PDT input
console.log(JSON.stringify(now));
// "2024-06-15T17:30:00.000Z" ← always UTC, regardless of local timezone
// Date inside an object
const event = {
title: 'Conference',
start: new Date('2024-06-15T10:30:00Z'),
end: new Date('2024-06-15T18:00:00Z'),
};
const json = JSON.stringify(event, null, 2);
// {
// "title": "Conference",
// "start": "2024-06-15T10:30:00.000Z",
// "end": "2024-06-15T18:00:00.000Z"
// }
// Date.prototype.toJSON() is what JSON.stringify calls internally:
new Date('2024-06-15T10:30:00Z').toJSON();
// "2024-06-15T10:30:00.000Z"
// Confirm the type — it's now a string in the JSON output
const parsed = JSON.parse(json);
console.log(typeof parsed.start); // "string" — NOT a Date object!The key point: JSON.stringify always converts Date objects to UTC ISO strings regardless of the local system timezone. A date created in New York time is stored as UTC in the JSON output. This is the correct and expected behavior — always store dates in UTC, convert to local time only for display.
Parsing dates from JSON: why strings stay strings
JSON.parse() converts JSON strings to JavaScript strings, JSON numbers to JavaScript numbers — but it does not convert ISO date strings to Date objects. You must call new Date(str) explicitly:
const json = '{"createdAt":"2024-06-15T10:30:00.000Z","count":42}';
const data = JSON.parse(json);
// ⚠️ createdAt is a plain string — NOT a Date object
console.log(typeof data.createdAt); // "string"
console.log(data.createdAt instanceof Date); // false
// Arithmetic on the raw string doesn't work as expected
// data.createdAt < new Date() → string/object comparison, unreliable
// ✅ Convert manually with new Date()
const createdAt = new Date(data.createdAt);
console.log(createdAt instanceof Date); // true
console.log(createdAt.getFullYear()); // 2024
// Validate before converting — guard against invalid strings
function safeParseDate(str) {
const d = new Date(str);
return isNaN(d.getTime()) ? null : d;
}
const date = safeParseDate(data.createdAt); // Date object or nullThis behavior is intentional: the JSON spec defines no rules for recognizing date patterns inside strings. If you need automatic conversion, use the reviver pattern shown in the next section.
Using a reviver function to auto-convert date strings
JSON.parse() accepts an optional second argument — a reviver function — that is called for every key/value pair in the parsed result. You can use it to detect ISO date strings and convert them to Date objects automatically:
// ISO 8601 date pattern — matches "YYYY-MM-DDTHH:mm:ss.sssZ" and variants
const ISO_DATE_RE = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}(.d+)?(Z|[+-]d{2}:d{2})$/;
function dateReviver(key, value) {
// Only process string values that look like ISO dates
if (typeof value === 'string' && ISO_DATE_RE.test(value)) {
return new Date(value);
}
return value;
}
const json = '{"title":"Release","publishedAt":"2024-06-15T10:30:00.000Z","views":1000}';
const data = JSON.parse(json, dateReviver);
console.log(data.publishedAt instanceof Date); // true
console.log(data.publishedAt.getFullYear()); // 2024
console.log(typeof data.title); // "string" (unchanged)
console.log(typeof data.views); // "number" (unchanged)
// Works recursively — nested dates are also converted
const nested = JSON.parse(
'{"user":{"name":"Alice","lastLogin":"2024-06-10T08:00:00Z"}}',
dateReviver
);
console.log(nested.user.lastLogin instanceof Date); // trueThe reviver pattern is the cleanest way to handle dates when your entire codebase works with Date objects. Be cautious with the regex: make it strict enough to avoid false positives on strings like version numbers or product codes that happen to match a loose date pattern.
// Python equivalent: object_hook for automatic date parsing
import json
from datetime import datetime, timezone
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
def date_hook(obj):
for key, value in obj.items():
if isinstance(value, str):
try:
obj[key] = datetime.fromisoformat(value)
except ValueError:
pass
return obj
json_str = '{"createdAt": "2024-06-15T10:30:00.000+00:00", "name": "Alice"}'
data = json.loads(json_str, object_hook=date_hook)
print(type(data['createdAt'])) # <class 'datetime.datetime'>
print(data['createdAt'].year) # 2024Unix timestamps: seconds vs milliseconds pitfall
Unix timestamps count the number of time units elapsed since the Unix epoch (1970-01-01T00:00:00 UTC). The critical difference: most server-side languages and APIs use seconds, but JavaScript's Date uses milliseconds.
// Seconds-based timestamp (Python, POSIX, most REST APIs)
import time
print(int(time.time())) # e.g. 1718447400
// Milliseconds-based (JavaScript)
console.log(Date.now()); // e.g. 1718447400000
console.log(new Date().getTime()); // same as Date.now()
// ⚠️ Classic bug: passing seconds directly to new Date()
const secondsTimestamp = 1718447400; // from a Python/Go/Rust API
const wrong = new Date(secondsTimestamp);
console.log(wrong.toISOString());
// "1970-01-21T05:34:07.400Z" ← year 1970! Off by 1000x
// ✅ Correct: multiply seconds by 1000 to get milliseconds
const correct = new Date(secondsTimestamp * 1000);
console.log(correct.toISOString());
// "2024-06-15T10:30:00.000Z" ← correct
// Converting back: JS Date → seconds for storage/API
const nowInSeconds = Math.floor(Date.now() / 1000);
console.log(nowInSeconds); // 1718447400A practical rule: if a timestamp value in your JSON is roughly 10 digits long (e.g. 1718447400), it is in seconds. If it is 13 digits long (e.g. 1718447400000), it is in milliseconds. Document which unit your API uses — always.
// Helper to handle either format safely
function toDate(timestamp) {
// Heuristic: timestamps before year 3000 in seconds are < 32503680000
const isSeconds = timestamp < 1e10;
return new Date(isSeconds ? timestamp * 1000 : timestamp);
}
console.log(toDate(1718447400).toISOString()); // seconds → correct
console.log(toDate(1718447400000).toISOString()); // milliseconds → correctCustom toJSON() method on objects
When JSON.stringify() encounters an object, it checks whether the object has a toJSON() method. If it does, the return value of that method is serialized instead of the raw object. You can use this to control exactly how your domain objects appear in JSON:
// Custom class with toJSON()
class Event {
constructor(title, startDate, endDate) {
this.title = title;
this.startDate = startDate; // Date object
this.endDate = endDate; // Date object
}
// Called automatically by JSON.stringify
toJSON() {
return {
title: this.title,
// Store as ISO string with explicit UTC label
startDate: this.startDate.toISOString(),
endDate: this.endDate.toISOString(),
// Include a computed Unix timestamp (seconds) for APIs that need it
startTimestamp: Math.floor(this.startDate.getTime() / 1000),
// Duration in minutes for convenience
durationMinutes: Math.round(
(this.endDate - this.startDate) / 60000
),
};
}
}
const event = new Event(
'Team Sync',
new Date('2024-06-15T10:00:00Z'),
new Date('2024-06-15T11:00:00Z')
);
console.log(JSON.stringify(event, null, 2));
// {
// "title": "Team Sync",
// "startDate": "2024-06-15T10:00:00.000Z",
// "endDate": "2024-06-15T11:00:00.000Z",
// "startTimestamp": 1718445600,
// "durationMinutes": 60
// }toJSON() is also how Date.prototype.toJSON() itself works internally — it is not magic built into JSON.stringify; it is a method the engine calls when it finds one. You can override it on any object, including by monkey-patching prototypes (though that is rarely advisable in production code).
Comparison: ISO string vs Unix timestamp vs custom format
Choosing the right date representation for your JSON API involves trade-offs across readability, precision, interoperability, and tooling support.
| Criterion | ISO 8601 string | Unix timestamp (s) | Unix timestamp (ms) | Custom string |
|---|---|---|---|---|
| Example value | "2024-06-15T10:30:00Z" | 1718447400 | 1718447400000 | "15/06/2024 10:30" |
| JSON spec type | string | number | number | string |
| Human-readable | Yes | No | No | Yes |
| Timezone-aware | Yes (explicit suffix) | Always UTC | Always UTC | Rarely |
| Sortable as-is | Yes (UTC strings only) | Yes | Yes | No |
| JSON-LD / Schema.org | Required | Not supported | Not supported | Not supported |
| JS new Date() support | Native | new Date(ts * 1000) | new Date(ts) | Locale-dependent |
| Sub-millisecond precision | No (ms is minimum) | No | Yes | Possible |
| Recommended for | REST APIs, JSON-LD, logs | POSIX systems, DB epochs | JS-to-JS communication | Avoid in APIs |
For new APIs, default to ISO 8601 UTC strings. They are the safest choice: readable, unambiguous, supported natively by every major language, and required by JSON-LD fields like datePublished and dateModified. Reserve Unix timestamps for performance-critical paths or when interoperating with systems that already use them.
Frequently asked questions
Does JSON support date objects natively?
No. The JSON specification (RFC 8259) defines only six value types: string, number, object, array, boolean, and null. There is no date type. Dates must be encoded as either an ISO 8601 string (e.g. "2024-06-15T10:30:00.000Z") or a number (Unix timestamp in seconds or milliseconds).
What is the recommended date format for JSON APIs?
ISO 8601 UTC strings are the de facto standard: "YYYY-MM-DDTHH:mm:ss.sssZ". This format is human-readable, sortable as a plain string, unambiguous (the "Z" suffix denotes UTC), and natively supported by JavaScript's Date constructor. Major specifications including RFC 3339 and JSON-LD's datePublished/dateModified fields require this format.
Why does JSON.parse not convert date strings to Date objects?
JSON.parse() follows the JSON specification strictly — it converts JSON strings to JS strings, JSON numbers to JS numbers, and so on. There is no rule in the spec that maps a string matching an ISO date pattern to a Date object. To auto-convert date strings during parsing, pass a reviver function as the second argument to JSON.parse().
How do I preserve the timezone in a JSON date?
Include the UTC offset explicitly in the ISO 8601 string. Use "Z" for UTC (e.g. "2024-06-15T10:30:00Z"), or a named offset like "+05:30" for IST (e.g. "2024-06-15T16:00:00+05:30"). A date string without any timezone suffix (e.g. "2024-06-15T10:30:00") is ambiguous — different runtimes may treat it as local time or UTC, causing subtle bugs.
What is the difference between Unix timestamp in seconds and milliseconds in JSON?
Most server-side languages and APIs (Python's time.time(), POSIX, database epoch columns) emit Unix timestamps in seconds since the Unix epoch (1970-01-01T00:00:00Z). JavaScript's Date.now() and new Date().getTime() return milliseconds. A common bug: passing a seconds-based timestamp directly to new Date() produces a date in 1970 instead of the correct year. Always multiply by 1000 when converting a seconds timestamp to a JS Date: new Date(secondsTimestamp * 1000).
How do I compare two dates that came from JSON?
Convert both strings to Date objects first with new Date(str), then compare using .getTime() for numeric comparison: dateA.getTime() < dateB.getTime(). Alternatively, ISO 8601 UTC strings can be compared directly as strings using the < and > operators because the format is lexicographically ordered — but only when both strings are in UTC and use the same precision.
Validate your JSON date fields
Paste a JSON payload with createdAt, updatedAt, or any date field into Jsonic to check formatting and structure before shipping to production.