JSON
JSON has no date type — pick a strict string format and document it.
The JSON specification defines exactly six value types: string, number, object, array, boolean, and null. Dates and times are not among them. In practice the ecosystem has converged on representing date-time values as RFC 3339 strings — e.g. "2026-06-05T14:00:00Z" — because RFC 3339 is unambiguous, human-readable, lexicographically sortable (for UTC values), and widely supported by parsers in every major language.
Epoch numbers (integers like 1780668000) appear frequently but carry two sources of ambiguity: the unit (seconds or milliseconds — JavaScript’s Date.now() uses milliseconds; Unix time() uses seconds) and the zone semantics (UTC is the usual assumption but is rarely documented). Without an explicit API contract, a consumer reading an epoch number cannot safely determine the unit or confirm the zone assumption.
Go deeper: even the "number" type is less portable than it looks
JSON’s grammar puts no limit on a number’s range or precision — how one is
represented is left entirely to the implementation, and the spec’s own
interoperability note warns that anything beyond IEEE-754 double precision
may not survive the trip. In practice that warning is the reality:
JSON.parse in JavaScript produces doubles, and many parsers in other
languages do the same unless configured otherwise, while Python decodes the
same document to arbitrary-precision integers and a Java parser may choose
long or BigInteger. The same bytes can decode to different values in
different consumers — assume the worst hop treats every number as a double.
For timestamps, a double sets a hard ceiling: integers are exact only up to
253. Millisecond epochs stay exact for roughly ±285,000 years —
fine. Microsecond epochs are exact only until the year 2255. A
nanosecond epoch is already past the limit: today’s value (about
1.78×1018) silently rounds to the nearest few hundred nanoseconds
in any double-based parser. And a fractional-seconds epoch like
1780668000.123 is a double by construction, inheriting binary
representation error before any parser touches it. If a pipeline must carry
high-precision epochs, carry them as strings — or accept the double as your
lowest common denominator.
Choose one format, apply it consistently across your entire API surface, document it explicitly in your schema or API contract, and validate incoming values against it. Mixing formats within a single service is a persistent source of integration bugs.
Choosing a format
RFC 3339 is the right default for new APIs. Use a Z suffix (UTC) or an explicit numeric offset. If you have a choice between seconds and milliseconds for epoch integers, at least pick one and state it in your documentation. Never mix the two in the same API.
Pitfall: Accepting both "2026-06-05T14:00:00Z" (string) and 1780668000 (number) in the same field across different endpoints or versions. Consumers must then handle two types for one semantic concept, and the number form silently loses zone information compared to the string form.
See also: RFC 3339 and Your own client ↔ server.
Go deeper: RFC 3339 vs ISO 8601 in JSON APIs
RFC 3339 is a profile of ISO 8601 — it restricts the grammar to the unambiguous subset most useful for network protocols. Key differences: RFC 3339 requires a complete date and time (no truncated forms like 2026-06 alone), requires a time offset (Z or ±HH:MM), and forbids certain ISO 8601 extensions like week dates and ordinal dates. For JSON APIs, RFC 3339 is the safer choice because its grammar is smaller and parsers are more consistent.
Z (UTC offset zero) and +00:00 are semantically equivalent in RFC 3339, and a compliant parser treats both as the same absolute instant. The real hazard is a string with no offset at all: JavaScript’s Date, for example, parses a date-only string like 2026-06-05 as UTC midnight but a date-time string without an offset like 2026-06-05T14:00:00 as local time. Always include an explicit offset (Z or ±HH:MM) so the instant is unambiguous.
OpenAPI 3.x defines format: date-time as an RFC 3339 date-time string. If your API uses OpenAPI, lean on this format annotation and generate validators from it rather than writing ad hoc regex checks.