Quick answer: JSON has no date type. Store dates as ISO 8601 strings in UTC, like
"2026-06-23T14:30:00Z". Parse them withnew Date()in JavaScript ordatetime.fromisoformat()in Python, and validate them with JSON Schema'sdate-timeformat.
Here is a question that trips up almost every developer the first time: how do you put a date in JSON? You open the spec, look for a date type, and find nothing. JSON supports strings, numbers, booleans, null, objects, and arrays. That is it. No dates, no times, no timezones.
That gap is why APIs ship dates in a dozen different shapes, why JSON.parse() hands you a string when you expected a Date, and why a "2026-06-23" created in Tokyo shows up as June 22 in New York. This guide covers the format you should use, how to parse it in every major language, how to generate random dates for test fixtures, and how to validate date fields so bad data never reaches production.
Explore a valid sample in the JSON Viewer to check its fields and nesting. The viewer does not perform the protocol-specific validation discussed in this guide.
Why JSON Has No Date Type
JSON was carved out of JavaScript object literal syntax in the early 2000s. Douglas Crockford kept it deliberately minimal so that any language could implement a parser in an afternoon. Dates were left out because there was no portable representation that every language agreed on. A Date in JavaScript, a datetime in Python, and a LocalDateTime in Java all behave differently.
The practical consequence: a date in JSON is always encoded as either a string or a number, and the interpretation is a contract between the producer and the consumer. If that contract is not written down, bugs follow.
The 4 Ways People Write Dates in JSON
You will meet all of these in the wild. Only one of them is a good default.
1. ISO 8601 string (recommended)
{
"createdAt": "2026-06-23T14:30:00Z",
"dueDate": "2026-07-01",
"meetingAt": "2026-06-23T20:00:00+05:30"
}
ISO 8601 is the international standard for date and time strings. The Z suffix means UTC. The +05:30 suffix is an explicit offset. A bare 2026-07-01 is a calendar date with no time. The web profile of this standard is RFC 3339, which is what JSON Schema, OpenAPI, and most API style guides reference.
Three reasons it wins: it is readable by humans, it sorts correctly as plain text (so ORDER BY created_at works even on a string column), and every mainstream language parses it without a third-party library.
2. Unix timestamp (number)
{
"createdAt": 1782311400,
"updatedAt": 1782311400000
}
A Unix timestamp counts seconds since January 1, 1970 UTC. It is compact and easy to do math on. The trap is the unit. JavaScript uses milliseconds, most Unix tools and Python's time.time() use seconds, and the two values above look like the same timestamp but are off by a factor of 1,000. If you use timestamps, name the field so the unit is obvious: createdAtMs or created_at_unix.
3. Microsoft JSON date (legacy)
{ "createdAt": "/Date(1782311400000)/" }
Older ASP.NET services emitted this format. It is a string wrapping a millisecond timestamp. Nothing parses it natively; you have to pull the number out with a regular expression. Treat it as a migration problem, not a format to adopt.
4. Locale strings (avoid)
{ "createdAt": "06/23/2026" }
Is that June 23 or the 6th day of the 23rd month? Americans read MM/DD/YYYY, most of the world reads DD/MM/YYYY, and new Date("06/23/2026") will silently interpret it in whatever way the runtime feels like. Never serialize dates with toLocaleDateString() or similar.
Timezones: The Bug That Ships at Midnight
The single most common date bug in JSON APIs is dropping the timezone. Consider this payload:
{ "startsAt": "2026-06-23T00:00:00" }
No Z, no offset. In JavaScript, new Date("2026-06-23T00:00:00") is parsed as local time. The same string parsed on a server in UTC and a browser in California produces two different instants, seven hours apart. Users in one timezone see the event on the wrong day.
The rule that prevents it:
- Store and transmit in UTC. Always end timestamps with
Z. Convert to local time only at the display layer. - Use calendar dates for calendar things. A birthday or a due date is
"2026-07-01", not"2026-07-01T00:00:00Z". Attaching midnight UTC to a date is how a July 1 deadline becomes June 30 in the Americas. - Keep the user's offset only when it carries meaning. A flight departure at
+09:00matters because it happens in Tokyo. For mostcreatedAtfields, it does not.
How to Parse JSON Dates in JavaScript
JSON.parse() never creates Date objects. Every date comes back as a string, and you have two ways to convert it.
Convert after parsing
const data = JSON.parse(payload);
const createdAt = new Date(data.createdAt); // Date object
console.log(createdAt.toISOString()); // "2026-06-23T14:30:00.000Z"
Use a reviver to convert automatically
A reviver function runs on every key/value pair during parsing. This one detects ISO 8601 strings and turns them into Date objects in one pass:
const ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2}))?$/;
function dateReviver(key, value) {
if (typeof value === "string" && ISO_DATE.test(value)) {
const d = new Date(value);
if (!isNaN(d)) return d;
}
return value;
}
const data = JSON.parse(payload, dateReviver);
data.createdAt instanceof Date; // true
Going the other direction, JSON.stringify() already calls Date.prototype.toJSON(), which returns an ISO 8601 UTC string. So serializing is free. For a deeper look at revivers and replacers, see our JSON.parse() and JSON.stringify() guide.
How to Parse JSON Dates in Python, Java, Go, and C#
Every language has the same two-step: parse the JSON, then parse the string.
Python
import json
from datetime import datetime
data = json.loads(payload)
created = datetime.fromisoformat(data["createdAt"]) # Python 3.11+ accepts the "Z" suffix
# Serializing: datetime is not JSON serializable by default
json.dumps({"createdAt": created}, default=lambda o: o.isoformat())
Java
// Jackson with JavaTimeModule
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Instant created = Instant.parse("2026-06-23T14:30:00Z");
Go
type Event struct {
CreatedAt time.Time `json:"createdAt"` // encoding/json uses RFC 3339 automatically
}
C#
// System.Text.Json reads and writes ISO 8601 by default
var evt = JsonSerializer.Deserialize<Event>(payload);
DateTimeOffset created = evt.CreatedAt;
Notice that Go, C#, and Jackson all default to ISO 8601. Pick that format and the serializers on both ends agree without configuration.
Generating Random Dates for JSON Test Data
Fixtures, seed scripts, load tests, and mock APIs all need realistic dates. Hardcoding "2024-01-01" in every record hides sorting and pagination bugs. Here is how to generate random dates that look like production data.
Plain JavaScript
function randomDate(start, end) {
const ms = start.getTime() + Math.random() * (end.getTime() - start.getTime());
return new Date(ms);
}
const users = Array.from({ length: 5 }, (_, i) => ({
id: i + 1,
createdAt: randomDate(new Date("2025-01-01T00:00:00Z"), new Date()).toISOString(),
birthDate: randomDate(new Date("1970-01-01"), new Date("2005-12-31"))
.toISOString()
.slice(0, 10), // calendar date only
}));
console.log(JSON.stringify(users, null, 2));
[
{
"id": 1,
"createdAt": "2025-09-14T03:21:47.912Z",
"birthDate": "1988-04-02"
},
{
"id": 2,
"createdAt": "2026-02-27T18:05:10.334Z",
"birthDate": "1996-11-19"
}
]
Python
import json, random
from datetime import datetime, timedelta, timezone
def random_date(start, end):
delta = end - start
return start + timedelta(seconds=random.randrange(int(delta.total_seconds())))
start = datetime(2025, 1, 1, tzinfo=timezone.utc)
end = datetime.now(timezone.utc)
records = [{"id": i, "createdAt": random_date(start, end).isoformat()} for i in range(1, 6)]
print(json.dumps(records, indent=2))
Faker
If you already use a fake-data library, it does the range math for you:
import { faker } from "@faker-js/faker";
const order = {
id: faker.string.uuid(),
placedAt: faker.date.between({ from: "2025-01-01", to: new Date() }).toISOString(),
shipBy: faker.date.soon({ days: 7 }).toISOString().slice(0, 10),
};
Three rules for random test dates: always emit ISO 8601 so the fixture matches production, keep createdAt earlier than updatedAt so ordering logic is exercised honestly, and seed the random generator in CI so a failing test reproduces.
Validating Date Fields with JSON Schema
A syntax validator confirms the JSON parses. It cannot tell you that "2026-13-45" is not a date. For that, add a schema:
{
"type": "object",
"properties": {
"createdAt": { "type": "string", "format": "date-time" },
"dueDate": { "type": "string", "format": "date" },
"updatedAtMs": { "type": "integer", "minimum": 0 }
},
"required": ["createdAt"]
}
date-time requires a full RFC 3339 timestamp with a timezone. date requires YYYY-MM-DD. One gotcha: ajv ignores format unless you install ajv-formats:
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv();
addFormats(ajv);
const validate = ajv.compile(schema);
validate({ createdAt: "2026-06-23T14:30:00" }); // false - missing timezone
validate({ createdAt: "2026-06-23T14:30:00Z" }); // true
That first failure is the midnight bug from earlier, caught before it reaches a database. For the full schema walkthrough, read JSON Schema explained.
Checklist for Dates in JSON
- Serialize as ISO 8601 / RFC 3339 strings.
- Timestamps end in
Zor carry an explicit offset. Never omit the timezone. - Calendar dates use
YYYY-MM-DDwith no time component. - If you must use numbers, put the unit in the field name.
- Convert to
Dateobjects after parsing, or use a reviver. - Validate with JSON Schema
format: date-timeorformat: date. - Generate random test dates in the same format production uses.
Before any of this, make sure the document itself is valid. A missing comma after a date field produces the same parse error as any other missing comma, and JSON parse errors rarely say which field caused them. Paste the payload into JSON Prettifier, click Validate, and get the exact line.