Serializing and Storing Temporal Values

How to serialize each Temporal type to the string and column that round-trips it losslessly. Part of Modern Temporal API.

Problem framing

Persisting a Temporal value is easy to get subtly wrong because each type carries different information and a database column rarely matches it exactly. Store a ZonedDateTime as a bare UTC timestamp and you lose the zone, so you can no longer reconstruct the user's wall clock after a rule change. Store a PlainDate in a timestamptz column and the driver bolts on a midnight and a zone, reintroducing the very ambiguity PlainDate exists to avoid. The rule is to serialize each type to the string form that round-trips it losslessly, and to pick the column that stores exactly those bits.

Match the type to the columnA UTC timestamp loses the zone of a ZonedDateTimeMatch the type to the columnstore ZonedDateTime as UTC mszone discardedstore the full ISO with [zone]reconstructs wall clockEach Temporal type has a canonical string; persist that, not a lossy projection.

API reference

Every Temporal type has a toString() that emits a canonical, machine-parseable form, and a matching from() that round-trips it.

Serialization formsCanonical strings per typeSerialization formsInstant.toString()'2024-03-15T14:30:00Z' — store as timestamptz/UTCZonedDateTime.toString()'...+09:00[Asia/Tokyo]' — needs a text columnPlainDate.toString()'2024-03-15' — store as DATEDuration.toString()'PT1H30M' ISO-8601 duration

Approach A: legacy Date

With Date, the only safe serialization is toISOString() (UTC). It cannot represent a zone or a zoneless civil date, so applications historically stored a second column for the IANA zone alongside the timestamp.

const row = { at: new Date().toISOString(), zone: 'America/New_York' }; // two columns

Serializing with legacy DatetoISOString is UTC-only; zone and civil date need side channelsLegacyModerntoISOString() → UTCseparate zone columnno civil-date typemanual reassemblyZonedDateTime.toString()zone travels in the stringPlainDate for civilself-describing

Approach B: Temporal

Serialize with toString(), deserialize with from(). The zone rides inside the ZonedDateTime string, so a single text column reconstructs everything.

import { Temporal } from '@js-temporal/polyfill';
const s = zdt.toString();                       // '2024-03-15T09:00:00-04:00[America/New_York]'
const back = Temporal.ZonedDateTime.from(s);     // fully reconstructed, DST-aware

Lossless round-triptoString to persist, from to reconstructLossless round-tripZonedDateTimetoString()→ textfrom() →same value

Production implementation

A tiny codec centralizes the mapping so every layer serializes consistently, and a JSON reviver rehydrates values on the way in.

import { Temporal } from '@js-temporal/polyfill';
export const codec = {
  toJSON: (v: Temporal.ZonedDateTime | Temporal.PlainDate) => v.toString(),
  reviveInstant: (s: string) => Temporal.Instant.from(s),
  reviveZoned:   (s: string) => Temporal.ZonedDateTime.from(s),
  reviveDate:    (s: string) => Temporal.PlainDate.from(s),
};

A serialization codecOne place maps types to strings and backA serialization codecvaluetoString()store/transmitfrom() revive

Edge cases

Serialization edge casesColumn mismatches and offset driftSerialization edge casesPlainDate in timestamptzdriver adds midnight+zoneuse DATE columnOffset-only string'-04:00' without [zone]future DST unknownJSON round-tripDate auto-UTCuse a reviver

Gotchas & common pitfalls

Serialization pitfallsLossy columns and offset-only storageWrongRightZonedDateTime → UTC mszone loststore full ISO with [zone]reconstructablestore offset, not [zone]stale after DST rule changekeep the IANA idfuture-proof

Testing checklist

Serialization test matrixEvery type round-trips through storageAssertions that prove the edge caseInstant → text → InstantequalZonedDateTime round-tripzone preservedPlainDate in DATE colno time addedJSON.parse w/ revivertyped value

Frequently Asked Questions

How should I store a Temporal.ZonedDateTime in a database?

Serialize it with toString(), which yields a self-describing string like '2024-03-15T09:00:00-04:00[America/New_York]', and store it in a text column. That preserves the IANA zone, so you can reconstruct the exact wall-clock time even after future DST-rule changes — something a bare UTC timestamp cannot do.

Can I store a Temporal.PlainDate in a timestamp column?

Avoid it. A timestamp or timestamptz column forces a time-of-day and often a zone onto the value, reintroducing the ambiguity PlainDate is designed to remove. Use a DATE column and store plainDate.toString() ('2024-03-15').

How do I serialize Temporal values to JSON?

Call toString() when writing (many types also implement toJSON) and use a reviver that calls the matching Temporal.*.from() when reading. A plain JSON.parse leaves them as strings, so a reviver is what restores the typed values.