プログラミング
TSON – イミュータブルでハッシュ固定スキーマを持つJSONスーパーセット
TSON – A JSON superset with immutable, hash-pinned schemas (tson.io)
要約
TSONは、イミュータブルでハッシュ固定スキーマを持つJSONのスーパーセットです。スキーマ定義自体がデータであり、ハッシュによって検証チェーン全体が保証されます。データフォーマットはUnicodeファーストのJSONスーパーセットで、より記述しやすいように設計されています。
全文翻訳
イミュータブルなスキーマを持つデータ。TSON(Typed Schema Object Notation)は、定義自体がデータであるイミュータブルでハッシュ固定スキーマを持つスキーマシステムです。ドキュメントはそのスキーマを参照し、スキーマはそのメタスキーマを参照します。1つのハッシュがチェーン全体を検証します。そして、TSONのデータフォーマットは、実際に記述を楽しめるUnicodeファーストのJSONスーパーセットです。
ガイドを読む
仕様
TSONはハッシュでスキーマを固定します!!
schema:"https://example.com/people.tn?sha256=c4d5e6f7…a2b3c4d5" !employee { name: "Ada Lovelace" born: 1815-12-10 department: Research level: L2 }
TSONスキーマはメタスキーマを固定します!!
id:"https://example.com/people.tn?sha256=c4d5e6f7…a2b3c4d5" !!meta:"https://tson.io/2026/32/m/meta.tn?sha256=8b1e4a9c…d7f2a640" !!import:"https://tson.io/2026/32/m/core.tn?sha256=3f9d2c71…b8e5c194" { person => { name: text born: date } employee => person & { department: text level: rank ~ L1 } rank => !enum [L1 L2 L3] }
TSONデータとスキーマのドキュメントは、同じLexer、同じツール、同じ検証を共有します。スキーマは、読み書きしやすいコンパクトな文法を持つマップです。このスキーマでは、personは2つのフィールドを持つレコードであり、employeeはpersonにdepartmentとlevelを追加したもので、levelフィールドはデフォルト値を持つenumです。そして、すべてのフィールドはデフォルトで必須です。
試してみる
読むのをスキップして、実行してみてください。
動作する実装があります: tson-java。
コマンドラインツールのinit-exampleコマンドは、例となるスキーマとデータドキュメントのペアを作成します。validateコマンドを使用して、データがスキーマに対してどのように検証されるかを確認できます。データを壊してみて(例: ageを引用符で囲む、フィールドを削除する)、パスと理由とともにすべての問題を一度に報告させることができます。
$ 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
OK
実装を入手する →
TSONスキーマ
しっかりした基盤の上に構築された簡潔なスキーマ
本来なら検証コードに散らばってしまうであろう契約を、宣言として記述します。基盤: JSONを理解し、構文やコードが書かれる前に、第一原理からスキーマとは何かを導き出す18記事の研究シリーズです。
§5.2レコードとフィールドの状態
フィールドはデフォルトで必須です。~ はデフォルト値を供給し、= は値を固定し、? はフィールドをオプショナルにします。5つの状態があり、それぞれが宣言で明示されます。
schema
config => { host: text port: integer ~ 8080 retries: integer = 3 comment: text? }
data
!config { host: "prod.db.internal" port: 5555 }
§5.3配列、タプル、セット
サイズ指定された均質な配列、固定形状のタプル、ユニークなメンバーを持つセット。3つの契約はすべて、データでは1つのブラケット構文としてエンコードされます。
schema
tags => [text; 1..10]
point => [number, number]
badges => set<text>
data (配列、タプル、セットはすべて配列としてエンコードされます)
!tags [urgent reviewed]
!point [3.5 7.2]
!badges [alpha beta]
§5.5アトムとenum
^ を使用してアトムの制約語彙を絞り込むか、コンストラクタを使用して新しいアトムファミリーを作成します。リファインメントはソースのIS-Aであり、enumはそのファミリー自体です。
schema
port => !integer ^ { min: 1 max: 65535 }
sku => !text ^ { pattern: "[A-Z]-[0-9]{3}" }
rank => !enum [L1 L2 L3]
data
!port 8080
!sku "A-123"
!rank L2
§5.8コンポジション
宣言された祖先を持つ新しいフィールド: ticket は audit の IS-A であり、貢献されたフィールドセットは互いに排他的である必要があります。サイレントなオーバーライドやダイヤモンドの曖昧さはありません。
schema
audit => { created: datetime updated: datetime? }
ticket => audit & { id: uuid title: text }
data
!ticket { created: 2026-07-01T09:00:00Z id: 550e8400-e29b-41d4-a716-446655440000 title: "Server unreachable" }
§5.7リファインメント
新しいフィールドを追加せずに継承されたフィールドを絞り込みます: デフォルト値は移動できますが、固定値は移動できません。契約のみが狭まります。1つの遷移テーブルがすべてのステップを管理します。
schema
service => { host: text port: integer ~ 8080 }
production => service ^ { port: = 443 }
data (portは443に固定されています; 値を繰り返す必要はありません)
!production { host: "prod.internal" }
§5.9サブトラクション
削除は許可されており、明示的です: public は account の代わりにはなりませんが、解決された出力はそれを正確に記録します。
schema
account => { user: text email: text password: text }
public => account - { password }
data
!public { user: "ada" email: "ada@example.com" }
§5.4選択
Sum型。構造的に排他的なバリアントはデータにタグを必要としませんが、重複するものはタグが必要です。リゾルバはエンコーディングごとにどちらかを導き出します。
schema
shape => (circle | rect)
circle => { radius: number }
rect => { width: number height: number }
shapes => [shape; 1..5]
data (レコードには型タグが必要です)
!shapes [ !circle { radius: 4 } !rect { width: 2 height: 3 } ]
§5.11フィールドグループ
Exactly-one-of、構造的に記述されます: データは、その自身のラベルの下に1つのメンバーを運び、合成タグは発明されません。
schema
contact => { name: text ( email: email | phone: text ) }
data (電話またはメールのいずれかのみが許可されます)
!contact { name: "Grace Hopper" phone: "555-0142" }
§5.10テンプレート
型パラメータと値パラメータの両方に対する、空白のある定義。完全にバインドされたアプリケーションは、解決されたスキーマ内の実際の名前付き型になります。
schema
paged => <T> { items: [T] cursor: text? }
retry_policy => <N> { attempts: integer ~ N }
checkout => retry_policy<5>
data
!checkout { attempts: 10 }
スキーマのバージョン管理
新しい必須フィールドを簡単に追加!!
id:"https://example.com/people-v2.tn?sha256=ac3f2e1a…a8e5be21" !!meta:"https://tson.io/2026/32/m/meta.tn?sha256=8b1e4a9c…d7f2a640" !!import:"https://tson.io/2026/32/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] }
バージョン2では、personレコードに新しい必須フィールドemailが追加されました。ミュータブルなスキーマシステムでは、新しい必須フィールドの追加は普遍的に禁止されています。Google、Microsoft、ZalandoのAPIガイドラインはすべてこの慣行を禁止しています。Protobufでさえ、requiredキーワードを危険なものとしてマークし、開発者が回避すべきだと述べています。これらのルールが存在するのは、単一の定義がそれに対して書かれたすべてのドキュメントに役立つように強制されるためです。TSONはこの負担を取り除きます。なぜなら、スキーマはイミュータブルだからです。バージョンは単に新しいドキュメントであり、新しいハッシュを持ちます。2つの契約は、完全な強度で共存し、personを構成するemployeeは、同じ宣言で新しい要件を継承します。
必須とは必須であること
レコードは、将来の拡張のために任意でフィールドが残されたり、永遠にトムストーンが運ばれたりすることなく、型に対して完全に閉じられたままです。
未知のフィールドはエラー
未知のフィールドを許容することは、インプレース進化の単なる回避策です。それがないと、単純なタイプミスはデータをサイレントにドロップするのではなく、即座にエラーを引き起こします。
バージョンによるルーティング
スキーマハッシュはヘッダーまたは最初の行にあり、即座に検証されます。単一のサーバーが複数のバージョンを同時にバインドするか、ゲートウェイがリクエストを異なるバックエンドにルーティングします。
マイグレーションは差分
両方のスキーマはデータであるため、ツールが正確な構造差分を計算します。マイグレーションは、数学的に正確な入力と出力契約を持つ純粋な変換になります。
完全なバージョニングの話 →
TSONデータ
読む価値のある表記
スキーマシステムは、より良いJSONを必要としたため、UnicodeファーストのJSONスーパーセットを手に入れました。
json
{ "name": "Ada Lovelace", "born": "1815-12-10", "fields": ["mathematics", "computing"], "verified": true, "note": null }
tson
{ name: "Ada Lovelace" born: !date 1815-12-10 fields: [mathematics computing] verified: true note: null }
変更点を示す
同じデータ、両側。
構文 JSONが必要とする | 意味 TSONが追加する
§2.4引用符はオプション
識別子とスカラー値は、Unicode識別子プロパティで定義された引用符なしのトークンであるため、ASCIIだけでなくすべてのスクリプトで機能します。引用符が必要なものだけを引用してください: スペース、コロン、自由テキスト。
json
"city": "Melbourne", "имя": "Алиса"
tson
city: Melbourne имя: Алиса
§2.4カンマは空白
任意の空白が項目を区切ります。カンマも引き続き機能するため、JSONの習慣を引き継ぐことができます。末尾のカンマは、どちらでもエラーのままです。
json
[1, 2, 3, 4, 5]
tson
[1 2 3 4 5]
§7.6実際に書ける数値
任意精度、16進数と2進数、および桁区切り文字は、注釈なしで解決されます。Inf