Schema and Data for AI.
TSON (Typed Schema Object Notation) gives AI structured output with one-shot correction. Schemas cost a third the tokens of JSON Schema and enforce more. Every document names its schema, every schema its meta-schema — one hash verifies the chain.
“The best diagnostics I've seen from any schema validator. Ten planted errors, all ten in one pass, each with the pointer, the constraint, and the value — where ajv gives you one must match schema at a time. For an LLM retry loop, this is one-shot repair.”
TSON data and schema documents share one lexer, one toolchain, one verification path. Both panes are editable, and everything you see runs in your browser. Every reference in the two headers is pinned by hash: the schema names its own content, its meta-schema and its type library, and the document names the schema. Edit the schema and those pins stop matching — a document's hash covers everything below the line that carries it, which is how it can name itself. That is what a consumer verifies before trusting a byte, and what the re-pin button restores.
Current status
A fully functioning Java reference implementation is available now, and a TypeScript port is also available — it is what validates the document above in your browser. The specification is published under the 2026 revision series and remains subject to change until it freezes as version 1. If the design interests you, this is the stage where feedback shapes it.
Implementations
Run it in your terminal.
The reference implementation is tson-java. The command-line tool's init-example command writes you an example schema and a data document pair. You can use the validate command to see how the data gets validated against the schema. Break the data (e.g. quote the age, delete a field) and it reports every problem at once, each with a path and a reason. The validator above is tson-typescript, a port that runs the same pipeline in the browser.
$ git clone https://github.com/litterat/ltr8-io-tson-java.git
$ cd ltr8-io-tson-java && ./gradlew :tson-cli:installDist
$ export PATH="$PWD/tson-cli/build/install/tson/bin:$PATH"
$ tson init-example
Wrote ./person.tn and ./person-data.tn.
$ tson validate person.tn person-data.tn
OKA succinct schema built on a solid foundation
Contracts you'd otherwise scatter through validation code, stated in declarations. The foundation: an eighteen-article research series understanding JSON and deriving what a schema is, from first principles, before any syntax or code was written.
Records and field states
Fields are required by default. ~ supplies a default, = pins a value, ? makes the field optional. Five states, every one explicit in the declaration.
config => {
host: text
port: integer ~ 8080
retries: integer = 3
comment: text?
}!config { host: "prod.db.internal" port: 5555 }Arrays, tuples, sets
Sized homogeneous arrays, fixed-shape tuples, unique-membered sets. Three contracts that all read as one bracket syntax in the data.
tags => [text; 1..10]
point => [number, number]
badges => set<text>!tags [urgent reviewed]
!point [3.5 7.2]
!badges [alpha beta]Maps with typed keys
A record has fixed named fields; a map has variable associations, and its key is a declared type — not every key flattened to a string. Sizes constrain the map, and ? admits an absent value.
by_id => {uuid => employee}
holidays => {date => text; 1..}
readings => {datetime => number?}!holidays { 2026-01-26 => "Australia Day" }
!readings { "2026-07-01T09:00:00Z" => _ }Atoms and enums
Refine an atom's constraint vocabulary with ^, or mint a fresh atom family with a constructor. A range narrows with min and max, a progression with multiple_of, a sparse set with members. The refinement IS-A its source; the enum is its own family.
port => !integer ^ { min: 1 max: 65535 }
sku => !text ^ { pattern: "[A-Z]-[0-9]{3}" }
status => !integer ^ { members: [200 404 500] }
rank => !enum [L1 L2 L3]!port 8080
!sku "A-123"
!status 404
!rank L2Composition
New fields with declared ancestry: ticket IS-A audit, and contributed field sets must be disjoint. No silent overrides, no diamond ambiguity.
audit => { created: datetime updated: datetime? }
ticket => audit & { id: uuid title: text }!ticket {
created: "2026-07-01T09:00:00Z"
id: 550e8400-e29b-41d4-a716-446655440000
title: "Server unreachable"
}Refinement
Tighten inherited fields without adding any: defaults can move, pins can't, contracts only narrow. One transition table governs every step.
service => { host: text port: integer ~ 8080 }
production => service ^ { port: = 443 }!production { host: "prod.internal" }Subtraction
Removal is allowed, and loud: public is not substitutable for account, and the resolved output records exactly that.
account => { user: text email: text password: text }
public => account - { password }!public { user: "ada" email: "ada@example.com" }Choice
Sum types. Structurally disjoint variants need no tag in data; overlapping ones require one. The resolver derives which, per encoding.
shape => (circle | rect)
circle => { radius: number }
rect => { width: number height: number }
shapes => [shape; 1..5]!shapes [
!circle { radius: 4 }
!rect { width: 2 height: 3 }
]Field groups
Exactly-one-of, stated structurally: the data carries one member under its own label, and no synthetic tag is invented.
contact => {
name: text
( email: email | phone: text )
}!contact { name: "Grace Hopper" phone: "555-0142" }Templates
Definitions with blanks, over type parameters and value parameters both. A fully bound application becomes a real, named type in the resolved schema.
paged => <T> { items: [T] cursor: text? }
retry_policy => <N> { attempts: integer ~ N }
checkout => retry_policy<5>!checkout { attempts: 10 }Extension points that stay verified
A position where the data names its own type. declared keeps the choice inside this schema; extern_of admits any type of one named foreign schema, which the data binds with its own pinned !!schema. The payload is validated in full against the type it names — this is not an escape hatch to unchecked data.
event => {
at: datetime
subject: declared
payload: extern_of<"https://example.com/claim.tn">
}!event {
at: "2026-07-01T09:00:00Z"
subject: !employee { name: "Ada Lovelace" }
payload:
!!schema:"https://example.com/claim.tn?sha256=ac3f2e1a…"
!claim { reference: "CLM-5678" }
}Schema Versioning
Adding a new required field with ease
!!id:"https://example.com/people-v2.tn?sha256=ac3f2e1a…a8e5be21"
!!meta:"https://tson.io/2026/35/m/meta.tn?sha256=8b1e4a9c…d7f2a640"
!!import:"https://tson.io/2026/35/m/core.tn?sha256=3f9d2c71…b8e5c194"
{
person => { name: text born: date email: text }
employee => person & { department: text level: rank ~ L1 }
rank => !enum [L1 L2 L3]
}Version 2 just added a new required email field to the person record. In mutable schema systems, adding a new required field is universally forbidden. The API guidelines for Google, Microsoft and Zalando all ban the practice. Protobuf even marks the required keyword a hazard developers must avoid. These rules exist because a single definition is forced to serve every document ever written against it. TSON removes this burden because schemas are immutable. A version is simply a new document with a new hash. Two contracts coexist at full strength, and employee composing person inherits the new requirement in the same declaration.
Required means required
Records remain completely closed under their type with no fields left arbitrarily optional for future-proofing and no tombstones carried forever.
Unknown fields are errors
Tolerating unknown fields is just a workaround for in-place evolution. Without it a simple typo triggers an immediate error instead of silently dropping data.
Route by version
The schema hash sits in the header or on the first line for instant validation. A single server binds multiple versions concurrently or a gateway routes requests to distinct backends.
Migration is a diff
Because both schemas are data a tool computes the exact structural diff. Migrations become pure transformations with mathematically precise input and output contracts.
A notation worth reading
The schema system required a better JSON, so it got a Unicode-first, JSON-like notation — the same braces, brackets and quoted strings, with the punctuation optional and the types real.
{
"name": "Ada Lovelace",
"born": "1815-12-10",
"fields": ["mathematics", "computing"],
"verified": true,
"note": null
}{
name: "Ada Lovelace"
born: !date 1815-12-10
fields: [mathematics computing]
verified: true
note: _
}Same data, both sides.syntax JSON requiresmeaning TSON adds
Quotes are optional
Identifiers and scalar values are unquoted tokens, defined over Unicode identifier properties, so this works in every script, not just ASCII. Quote only what needs quoting: spaces, colons, free text.
"city": "Melbourne", "имя": "Алиса"city: Melbourne имя: АлисаCommas are whitespace
Any whitespace separates items; commas still work, so JSON habits carry over. A trailing comma is fine too — TSON has no elision, so there is nothing for a stray comma to be confused with.
[1, 2, 3, 4, 5][1 2 3 4 5]Numbers you can actually write
Arbitrary precision, hex and binary, and digit separators resolve with no annotation at all. Infinities and NaN are approximate-tier values and need an explicit !float64; rationals and complex numbers need their own annotation too, since base resolution treats them as strings otherwise.
"mask": 255, "budget": 1000000,
"ratio": 3.142857142857143, "limit": "Infinity"mask: 0xFF budget: 1_000_000 ratio: !rational "22/7"
atoms: 6.02e23 limit: !float64 .inf gap: !float64 .nanStrings with more than one line
Triple-quoted blocks with common indentation stripped, so multi-line text stays readable in source and exact in value.
"poem": "Roses are red\nViolets are blue"poem: """
Roses are red
Violets are blue
"""Maps with real keys
JSON objects force every key into a string. TSON keeps records (fixed named fields) and maps (variable associations) distinct, and map keys can be any value.
{ "1": "one", "2026-03-13": "launch" }{ 1 => one 2026-03-13 => launch }Absent is not null
JSON gives two states — key missing, or null — to do three jobs. TSON has no null at all: the absent sentinel _ says "this position exists and holds no value", and it works in an array, where a missing key cannot.
"phone": nullphone: _ scores: [1 _ 3 _ 5]Metadata that travels with the value
Annotations attach ordered, preserved metadata to any value: no more "_comment" fields pretending to be data. This is also why TSON needs no comment syntax: annotations survive parsing.
"phone_comment": "deprecated, use mobile",
"phone": "555-0100"phone: @deprecated:"use mobile" "555-0100"Types when you want them
Type annotations tag a value with what it is. A built-in vocabulary — dates, times, UUIDs, bytes, networks, exact decimals — works with no schema at all, and the value parses to the native type, not a string. An elapsed duration and a calendar period are separate types, because a month has no fixed length and a second does.
"id": "550e8400-e29b-41d4-a716-446655440000",
"price": 19.99, "ttl": "PT30M"id: !uuid 550e8400-e29b-41d4-a716-446655440000
price: !number 19.99 ttl: !duration PT30M
term: !period P1Y avatar: !bytes "iVBORw0KGgo="The Developer Guide
The chain walked end to end, versioning, worked examples, and the reasoning behind the design.
Draft Specifications
Text Data Format, Type System and Schema, Meta Schemas and Core types.
The proto-schema research
The schema model, derived from the physical constraints of serialized data.
What comes next
One schema. Multiple encodings.
The TSON Schema and its underlying type system are agnostic to the encoding; the same architecture as ASN.1, where the schema is the product and formats serve it. The TSON text data format is the first encoding, but won't be the only one. The plan is to develop encoding rules for JSON, compact formats like TOON, and binary, each carrying the same schema-verified values.