JSON Date Format: How to Handle Timestamps
JSON has no date type. RFC 8259 gives you strings, numbers, booleans, null, arrays and objects, and that is the entire list. Every timestamp you have ever seen in an API response is a convention layered on top of a string or a number, which is why created_at deserializes cleanly in one language and throws in the next.
Three Formats Worth Using
| Format | Example | Use it for | The catch |
|---|---|---|---|
| ISO 8601 with an explicit offset | "2026-08-09T14:30:00Z" | Almost everything | Verbose, and the offset gets dropped by careless producers |
| Date only | "2026-08-09" | Birthdays, invoice dates, anything without a moment attached | Datetime parsers silently widen it to midnight in some zone |
| Unix epoch seconds | 1786396200 | JWT claims, high-volume logs | No timezone, no sub-second precision, easy to confuse with milliseconds |
Z or +02:00 on the end. The main exception is JWTs, where exp, iat and nbf are epoch seconds because RFC 7519 says so, and you should not invent your own convention there.
What you should not ship is a locale string like "08/09/2026", which is August 9th in the US and September 8th nearly everywhere else, or the old .NET "/Date(1786396200000)/" wrapper. Both force every consumer to write a custom parser.
Seconds Versus Milliseconds
This is the most expensive bug in the category, because nothing errors. It just gives you 1970:
JavaScript wants milliseconds. Python's datetime.fromtimestamp and Go's time.Unix want seconds. A timestamp near 1.7e9 is seconds, one near 1.7e12 is milliseconds, and that gap is wide enough to disambiguate reliably. Pydantic implements exactly that heuristic and treats anything above 2e10 as milliseconds.
If you control the producer, send a string instead and the whole question disappears.
What the Generators Actually Emit
Take a payload with one of each kind of date field:
The JSON to TypeScript converter gives you this:
string, not Date, and that is correct. JSON.parse never produces a Date object. If your interface claims created_at: Date, the type is lying about what is in memory right after parsing, and the lie surfaces later as value.getTime is not a function. Convert at the boundary, then use a type with real Date fields on the far side of that conversion.
The other three targets do carry date types through, because they all have a parsing step where the conversion can happen. From the JSON to Zod converter:
From the JSON to Pydantic converter:
And from the JSON to Go converter:
Note the asymmetry in the Go output. created_at becomes time.Time but published_on stays a string, and that is deliberate. Go's time.Time unmarshals from RFC 3339 only, so a date-only value would fail at runtime with a parse error about the missing T. A string field that you convert yourself with time.Parse("2006-01-02", v) is the honest representation.
The Per-Language Gotchas
JavaScript treats the two string forms differently. A date-only string is parsed as UTC. A date-time string with no offset is parsed as local time. Same code, same shape of input, two different rules:That middle line moves by up to 14 hours depending on where the code runs, which is why the same test passes in CI and fails on a developer laptop.
Zod rejects offsets by default.z.iso.datetime() accepts Z and nothing else unless you ask for more:
If you are on Zod 4, use the z.iso namespace. The older z.string().datetime() and z.string().date() still work in 4.x but are deprecated.
int lands in a datetime field as a Unix timestamp, and a date-only string lands there as midnight, both without complaint. Useful when you are consuming an API you do not control, and a problem when you wanted the strictness. The other thing to know is the serialization split:
model_dump() gives you real datetime objects, so passing its result straight to json.dumps raises TypeError: Object of type datetime is not JSON serializable. Use model_dump_json(), or model_dump(mode="json") when you need the dict.
The One Rule
Never emit a datetime without an offset. "2026-08-09T14:30:00" is the worst string in this article precisely because it works everywhere: it passes JSON validation, it parses in every language, and it means a different instant in each one. Ask for the Z, store UTC, and convert to a local zone only at the point where you render it.