TOML's four datetime types: Offset Date-Time, Local Date-Time, Local Date, Local Time
TOML has four datetime types, axed by whether timezone info is present and what parts (date / time / both) are meaningful. Coming from JSON (no datetime types) or YAML (timestamps via implicit typing), TOML asks you to be explicit about which form you want — which catches problems early but trips up writers who skim the spec.
The four types
Defined in TOML v1.0.0 §Date-Time:
| Type | Example | TZ info | Use |
|---|---|---|---|
| Offset Date-Time | 1979-05-27T07:32:00-07:00 | yes | absolute event times (logs, API timestamps) |
| Local Date-Time | 1979-05-27T07:32:00 | no | “local time” (calendar events) |
| Local Date | 1979-05-27 | no | date only (birthdays, release dates) |
| Local Time | 07:32:00 | no | time only (recurring schedules) |
Offset Date-Time
RFC 3339 format with timezone offset:
created = 1979-05-27T07:32:00-07:00
updated = 1979-05-27T15:32:00Z # Z = UTC Zis shorthand for+00:00.- Sub-second precision:
1979-05-27T00:32:00.999999Z. - Semantics: “this exact moment, anywhere in the world.”
For event logs, transaction timestamps, API response times — anything where the absolute moment matters globally.
Local Date-Time
Datetime without timezone:
event_starts = 1979-05-27T07:32:00 - Means “7:32 in the local clock,” but TOML alone doesn’t fix which timezone.
- Use for calendar events, reminders — situations where the observer’s local time is the relevant interpretation.
“Earthquake at 14:46 on March 11” written as Offset Date-Time pins the global moment in UTC. Written as Local Date-Time, it stays “2:46 PM whoever sees it.” Each suits a different use case.
Local Date
Date only:
release_date = 1979-05-27 No time component. For birthdays, deadlines, release dates — when the day itself is the data, not a particular moment in it.
Local Time
Time only:
opening_hours = 09:00:00 Not tied to a specific date. For “every day at 9,” recurring schedules.
Comparison to YAML / JSON
JSON
JSON has no datetime type. The convention is ISO 8601 strings:
{ "created": "1979-05-27T07:32:00-07:00" } Parsers return strings; consumer code parses with Date.parse or equivalent.
YAML
YAML has timestamps (!!timestamp tag):
created: 1979-05-27T07:32:00-07:00 But YAML’s implicit type inference comes with the Norway Problem and similar surprises, so derivatives like strictyaml strip implicit typing entirely.
TOML’s middle ground
TOML has explicit types but distinguishes Local vs Offset, forcing the writer to know which they want. More rigorous than JSON’s strings, more explicit than YAML’s inference.
Real-world usage
Cargo (Cargo.toml)
Rust project manifest. Versions are strings; license is a string; datetime types are uncommon in core fields. Where they show up is in custom extensions:
[package]
name = "my-crate"
version = "0.1.0"
rust-version = "1.65"
publish-date = 2024-03-15 # Local Date publish-date isn’t part of the Cargo spec but a user extension.
pyproject.toml
Python project config:
[project]
name = "my-package"
version = "0.1.0"
release-date = 2024-03-15
[tool.bumpversion]
current_version = "0.1.0"
last_bumped = 2024-03-15T10:00:00+09:00 The mainstream pyproject.toml fields are strings/arrays/tables. Datetime types appear in [tool.*] extension sections.
Hugo / Zola front matter
Static-site generators sometimes use TOML for post metadata:
+++
title = "Post title"
date = 2024-03-15T09:00:00+09:00
expiry_date = 2024-12-31
+++ date as Offset Date-Time (the absolute moment of publishing), expiry_date as Local Date (valid through that day) is the natural mapping.
Native types after parsing
What each parser produces:
| Language | TOML library | Offset Date-Time | Local Date-Time | Local Date | Local Time |
|---|---|---|---|---|---|
| Python 3.11+ | tomllib (stdlib) | datetime (tz-aware) | datetime (naive) | date | time |
| Rust | toml crate | OffsetDateTime (with time) or string | PrimitiveDateTime | Date | Time |
| Go | BurntSushi/toml | time.Time (zoned) | time.Time (no zone) | custom | custom |
| JavaScript | @iarna/toml | Date | Date (UTC-interpreted) | Date | custom |
JavaScript’s single Date type can’t express “time only” or “date only,” so information is lost on parse — a Local Time often becomes 1970-01-01T<hh:mm:ss> or similar.
Pitfalls
1. Forgetting Z and getting Local Date-Time silently
event = 1979-05-27T07:32:00 # Local Date-Time (NOT UTC!)
event = 1979-05-27T07:32:00Z # Offset Date-Time Without Z or +HH:MM, it’s Local Date-Time. Forgetting the Z and getting silent local-time semantics is the most frequent mistake.
2. Sub-second precision differences
event = 1979-05-27T07:32:00.123 # ms
event = 1979-05-27T07:32:00.123456 # μs
event = 1979-05-27T07:32:00.123456789 # ns (implementation-dependent) The spec allows arbitrary precision, but parsers may truncate at microseconds or nanoseconds. Verify your parser’s behavior if precision matters.
3. Storing Local Date-Time and discovering the timezone is unrecoverable
“Just store Local Date-Time and apply timezone later” sounds flexible but breaks irreversibly when the timezone information isn’t available downstream. If you can know the timezone at write time, use Offset Date-Time.
Cheat-sheet by use
- Event timestamps, logs, API responses → Offset Date-Time.
- Calendar events, “local 9 AM” semantics → Local Date-Time.
- Birthdays, release dates, deadlines → Local Date.
- Recurring “every day at” times → Local Time.
- Interop with non-TOML systems → ISO 8601 strings (outside the type system but universal).
Summary
TOML’s four datetime types sit between JSON’s “everything is a string” and YAML’s “guess the type for me.” The system asks you to be explicit, and the Offset-vs-Local distinction is the place that catches most writers — the difference between 1979-05-27T07:32:00 and 1979-05-27T07:32:00Z is “local time” vs “UTC,” and a missing Z is the most common bug.
To inspect TOML structure as JSON, the TOML to JSON tool shows the parsed shape side-by-side.