Every value says what it is.
Dates, timestamps, decimals, big integers, binary, geometry, time-of-day, and durations are part of the grammar — not conventions agreed in a README. The same rules hold for a single document and for a record stream.
# comments are part of the format { service: `checkout-api`, enabled: T, launch_on: DATE(2026-02-01), deploy_after: TIMESTAMP(2026-01-15T10:30:00Z), price_cap: DECIMAL(199.00), # scale is data account_id: BIGINT(9007199254740993), signing_key: BINARY(SGVsbG8=), boundary: Geometry("Polygon", [ [ [80.27, 13.08], [80.28, 13.08], [80.28, 13.09], [80.27, 13.08] ] ]), opens: Time("09:30"), ttl: Duration("PT45M"), regions: [`eu-west-1`, `us-east-1`] }
One root object. Every value carries its own kind.
@version(1.0) {at: TIMESTAMP(2026-01-15T10:30:00Z), level: `warn`} {at: TIMESTAMP(2026-01-15T10:30:04Z), level: `info`}
One document per line. No record may contain a raw newline, so a reader may split the file before parsing any of it.
Why STF
A format a reader can trust without a schema beside it.
Types in the syntax
DATE, TIMESTAMP, DECIMAL, BIGINT, BINARY, Geometry, Time, and Duration are grammar, so a reader never infers a type from a key name.
Record streams
A .stfs file is one document per line. Records parse independently, so a reader can report a bad one and carry on — though it stops by default, so corruption is never skipped in silence.
Exact decimals
DECIMAL(1.5) and DECIMAL(1.50) are different values. Scale survives the round trip, so money is representable.
Strict by design
Every rejection maps to exactly one documented code, and a conversion that would lose a type fails unless you ask for it in writing.
Canonical form
One byte encoding per value, so a document can be hashed, signed, and diffed byte-for-byte.
Editor and CLI
stf check, fmt, lint, canon, and convert, plus a language server that reports the same codes as CI.
How it compares
What each specification requires, including the row STF loses.
| Capability | STF | JSON | JSON5 | YAML | TOML | Ion |
|---|---|---|---|---|---|---|
| Dates and timestamps as a distinct type | ✅ | ❌ | ❌ | 🟡 | ✅ | ✅ |
| Exact decimals, scale preserved | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Integers beyond 2⁵³ without loss | ✅ | ❌ | ❌ | 🟡 | 🟡 | ✅ |
| Binary data as a distinct type | ✅ | ❌ | ❌ | 🟡 | ❌ | ✅ |
| Comments | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Duplicate keys rejected | ✅ | ❌ | ❌ | 🟡 | ✅ | ❌ |
| Record streams | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
| Splittable on newlines before parsing | ✅ | 🟡 | ❌ | ❌ | ❌ | ❌ |
| Compact binary encoding | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| One documented code per rejection | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Canonical form | ✅ | ❌ | ❌ | 🟡 | ❌ | 🟡 |
The last two rows are criteria STF set for itself, so every other format scores ❌ or 🟡 by construction — weigh them accordingly. Ion beats STF on payload size and parse throughput, which no column of ticks can show; comparison.md gives the long form, and the benchmarks give the numbers.
One specification, no dialects
Every rule below is normative, so two conformant parsers cannot disagree about a document.
§3— fourteen value kinds, defined independently of any host language.§13— parse(serialize(v)) ≡ v, and no constructor inferred from string content.§14— canonical form: one byte encoding per value.§15— a mandatory nesting depth limit, defaulting to 64.codes— one documented code per rejection; message text is not normative.
{account_id:BIGINT(9007199254740993),boundary:Geometry("Polygon", [[[80.27,13.08],[80.28,13.08],[80.28,13.09],[80.27,13.08]]]),deploy_after:TIMESTAMP(2026-01-15T10:30:00Z),enabled:T,launch_on:DATE(2026-02-01),opens:Time("09:30"),price_cap:DECIMAL(199.00),regions:["eu-west-1","us-east-1"],service:"checkout-api",signing_key:BINARY(SGVsbG8=),ttl:Duration("PT45M")}