TOML's four datetime types: Offset Date-Time, Local Date-Time, Local Date, Local Time

5 min read

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:

TypeExampleTZ infoUse
Offset Date-Time1979-05-27T07:32:00-07:00yesabsolute event times (logs, API timestamps)
Local Date-Time1979-05-27T07:32:00no“local time” (calendar events)
Local Date1979-05-27nodate only (birthdays, release dates)
Local Time07:32:00notime 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
  • Z is 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:

LanguageTOML libraryOffset Date-TimeLocal Date-TimeLocal DateLocal Time
Python 3.11+tomllib (stdlib)datetime (tz-aware)datetime (naive)datetime
Rusttoml crateOffsetDateTime (with time) or stringPrimitiveDateTimeDateTime
GoBurntSushi/tomltime.Time (zoned)time.Time (no zone)customcustom
JavaScript@iarna/tomlDateDate (UTC-interpreted)Datecustom

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.