💪

JSONはもう古い?LLM時代の新フォーマット「TOON」がトークンを節約する理由

に公開
5

はじめに

JSONくん、君はちょっと「おしゃべり」すぎるかもしれない。

開発をしていると、JSON (JavaScript Object Notation) とはもう「空気」のような存在ですよね。何かしらどこかしらで付き合いのある感じ。
APIを叩けばJSONが返ってくる。設定ファイルもJSON。何ならログもJSON。
私も長年連れ添った相棒として、彼(JSON)のことは信頼しています。

でも、LLM(大規模言語モデル) と付き合い始めてから、ちょっと気になることが出てきました。
「あれ…? 君、無駄口多くない…?」

[
  {"id": 1, "name": "user_a", "role": "admin"},
  {"id": 2, "name": "user_b", "role": "member"},
  {"id": 3, "name": "user_c", "role": "member"}
]

丁寧なんです。すごく丁寧。良いやつだ、彼は(彼じゃないかもしれないけれど)
でも、毎回 "id":, "name":, "role": って繰り返すその律儀さが、従量課金のトークン をガリガリ削っていく音が聞こえる気がするんです。
「塵も積もれば山となる」とは言いますが、LLM時代においてその塵は 「お金」「速度」 そのものです。

そこで最近、界隈でふと耳にしたのが TOON (Token-Oriented Object Notation) という概念。
単語が「漫画・アニメ」の意味の方かな?可愛い感じかな?なんて思いますが、中身はかなり合理的でシビアなやつでした。
いいね、そういうの私好きよ。

今回は、この「TOON」について、JSONとの付き合い方に悩み始めた私が調べたことをまとめてみます。


LLM時代におけるJSONの「贅肉」問題

JSONは、人間にも機械にも読みやすい、素晴らしいフォーマットです。これは揺るぎない事実。
ですが、「トークン効率」 という観点で見ると、彼はかなりリッチな服を着ています。

具体的にはこのあたりが「贅肉」です:

  1. 繰り返されるキー名: 配列の要素が増えるたびに "name": が増殖する。
  2. 大量の括弧: {}, [] のオンパレード。
  3. 引用符: 文字列を囲む "" も、積み重なれば結構な量。

これらは、従来のWeb APIなら「可読性」として歓迎される要素でした。
しかし、LLMにとっては 「意味のないトークン」 であり、「コンテキストウィンドウの浪費」 でしかありません。

そもそも「トークン」ってどう数えられてるの?

「文字数とトークン数は違う」というのはAIエンジニア的には衆知なのかな、という認識ですが、具体的にJSONがどうトークン化されるか、意識したことはありますか?
OpenAIのTokenizer(cl100k_base)を例に見てみましょう。
https://platform.openai.com/tokenizer

例えば {"id": 123} という短いJSON。
人間が見れば「idが123だな」と一瞬ですが、Tokenizerはこれを分解します(GPT-4oの場合)。

合計6トークンです。
(※トークン分割はモデルによって異なり、5〜7トークン程度になります)

たった一つのデータを送るのに、記号だけで半分以上のトークンを消費しています。
これが1万件のデータリストになったら…想像するだけでゾッとしますよね。

一方、TOON的な発想で、構造情報を先頭にまとめて [1,]{id}: 123 と表現した場合、
記号の繰り返しが削減され、より効率的になります。
この「塵」のような差が、RAGで数千文字のコンテキストを扱う際に、巨大な「山」となって効いてくるのです。

日本語ユーザーには特に「効く」かもと思った話

私たち日本語ユーザーにとって、この問題はさらに深刻です。
英語なら1単語≒1トークンですが、日本語は1文字で1〜2トークン消費する ことがザラにあります。
つまり、ただでさえ「データの中身(日本語)」でトークンを食うのに、その周りを「JSONの記号」でデコレーションするのは、高級な和牛を分厚い衣で包んで揚げるようなもの です。もったいない。そして、深夜に肉の話をしたら、お腹空いてきました、個人ミス…oh。

構造部分(衣)を極限まで薄くするTOONのアプローチは、トークン効率の悪い日本語を扱う私たちにこそ、必要な技術なのかもしれんかもな、と思いました。
採用するかは置いておいて。


TOON (Token-Oriented Object Notation) とは?

TOONは、その名の通り 「トークン効率を最優先に設計された」 データフォーマット(というより記法・概念)です。
JSONと互換性のあるデータ構造を持ちながら、極限まで記述を簡素化しています。

イメージとしては、「JSONをダイエットさせて、必要な骨組みだけ残した」 ような感じです。

百聞は一見にしかず、JSON vs TOON

論より証拠。見た方が早いですからね。
同じデータをJSONとTOONで表現して、その差を見てみましょう。

いつものJSON:

[
  {
    "id": 1,
    "name": "Alice",
    "role": "Admin",
    "status": "Active"
  },
  {
    "id": 2,
    "name": "Bob",
    "role": "User",
    "status": "Inactive"
  },
  {
    "id": 3,
    "name": "Charlie",
    "role": "User",
    "status": "Active"
  }
]

TOONの場合:

[3,]{id,name,role,status}:
1,Alice,Admin,Active
2,Bob,User,Inactive
3,Charlie,User,Active

…圧倒的じゃないですか?
あの大量の { } " key: が消え去り、データの実体だけが残っています。
先頭の [3,]{id,name,role,status}: で配列の長さとフィールド定義を明示し、以降はCSVライクにデータを並べるだけ。
YAMLのインデント構造とCSVの表形式を融合させたのがTOONの思想です。


実装してみる:PythonでJSONをTOONに変換

「概念はわかったけど、どうやって使うの?」という方のために、簡単な変換スクリプトを書いてみました。
標準ライブラリだけでサクッと書けます。

def json_to_toon(data: list[dict]) -> str:
    """
    辞書のリストを実際のTOON形式に変換する簡易関数
    """
    if not data:
        return ""
    
    # ヘッダー取得
    headers = list(data[0].keys())
    count = len(data)
    
    # TOON形式: [N,]{fields}: で開始
    header = f"[{count},]{{{','.join(headers)}}}:"
    lines = [header]
    
    # データ行の作成(CSV形式)
    for item in data:
        values = [str(item.get(h, "")) for h in headers]
        line = ",".join(values)
        lines.append(line)
        
    return "\n".join(lines)

# 試してみる
users = [
    {"id": 1, "name": "Alice", "role": "Admin"},
    {"id": 2, "name": "Bob", "role": "User"}
]

print(json_to_toon(users))

出力結果:

[2,]{id,name,role}:
1,Alice,Admin
2,Bob,User

これをプロンプトに埋め込んで、「以下のデータを元に考えて」とLLMに投げればいいわけです。
受け取る側(LLMからの出力)も、「このフォーマットで返して」と指定すれば、パースも比較的容易に行えます。

逆もまた然り:TOONをパースしてJSONに戻す

「送るのはいいけど、受け取ったデータをプログラムで扱えないと意味ないじゃん」
その通りです。LLMからTOON形式で出力されたテキストを、Pythonの辞書リストに戻すパーサーも用意しておきましょう。

def toon_to_json(toon_text: str) -> list[dict]:
    """
    TOON形式のテキストを辞書リストに変換する
    """
    lines = toon_text.strip().split('\n')
    if len(lines) < 2:
        return []

    # ヘッダー行の解析: [N,]{field1,field2}:
    header_line = lines[0]
    
    # フィールド部分を抽出
    import re
    match = re.match(r'\[(\d+),\]\{([^}]+)\}:', header_line)
    if not match:
        return []
    
    expected_count = int(match.group(1))
    headers = [h.strip() for h in match.group(2).split(',')]
    
    result = []
    # データ行の処理
    for line in lines[1:]:
        if not line.strip():
            continue
            
        values = [v.strip() for v in line.split(',')]
        
        if len(values) != len(headers):
            continue
            
        row_dict = {}
        for i, header in enumerate(headers):
            row_dict[header] = values[i]
            
        result.append(row_dict)
    
    # 配列長の検証
    if len(result) != expected_count:
        print(f"Warning: Expected {expected_count} items, got {len(result)}")
        
    return result

# テストデータ
toon_data = """
[2,]{id,name,role}:
1,Alice,Admin
2,Bob,User
"""

print(toon_to_json(toon_data))
# Output: [{'id': '1', 'name': 'Alice', 'role': 'Admin'}, {'id': '2', 'name': 'Bob', 'role': 'User'}]

もちろん、値の中にカンマ , が含まれる場合のエスケープ処理など、本番運用ではもう少し堅牢な作り込みが必要ですが、基本的なロジックはこれだけです。
「標準ライブラリがない」と言いましたが、これくらいのコード量ならユーティリティ関数としてコピペしても許される範囲ではないでしょうか。多分。


なぜ今、TOONなのか?

「いやいや、CSVでいいじゃん」と思った方。鋭いです。
でも、TOON(という概念)を採用するメリットは、単なる「文字数削減」以上の意味があります。

1. お財布に優しい(APIコストの削減)

LLMのAPI利用料はトークン従量課金が一般的です。
特に入力(Prompt)よりも出力(Completion)の方が高いケースが多いですよね。
RAG(検索拡張生成)などで、大量のドキュメントリストをLLMに渡すとき、JSONだとすぐにトークン上限に達してしまいますが、TOONなら 30%〜60% 近く圧縮できるケースもあります。

RAGに関しても割とデカい情報を持たせることが増えてきたので結構ここは知識として押さえておかんとかなあ、と思ったのが今回の記事書く切っ掛けです。へへ。

実測例:ユーザー3人のデータ(id, name, role, status)

  • Pretty JSON: 98 tokens
  • JSON (compact): 51 tokens
  • YAML: 63 tokens
  • TOON: 39 tokens
  • CSV: 29 tokens

TOONはPretty JSONから約60%削減、JSON compactからも約24%削減を実現しています。
ただし、最もトークン効率が高いのはCSVで、TOONはCSVより約34%多くなります。
これは、TOONが配列長宣言 [3,] やフィールド定義 {id,name,role,status}: により、構造情報を明示的に持たせているためです。

※実際のトークン数は、データの構造や内容によって大きく変動します。上記は小規模な均一データの例です。

これは、「同じお金で、より多くの情報を処理できる」 ということです。

コスト削減シミュレーション

「30%削減」と言われてもピンとこないかもしれないので、お金の話をしましょう。
例えば、GPT-4oを使って、毎日1万件のユーザーデータを処理するバッチ処理を走らせるとします。

  • JSONの場合: 1件あたり平均500トークン × 1万件 = 500万トークン
  • TOONの場合: 1件あたり平均300トークン × 1万件 = 300万トークン

GPT-4oの入力価格($5.00 / 1M tokens)で計算すると…

  • JSON: $25 / day
  • TOON: $15 / day

1日で$10(約1,500円)の差
「なんだ、ランチ代くらいか」と思いました?
これが月間だと 45,000円。年間だと 54万円 の差になります。
スタートアップとかのサーバー代としては、無視できない金額になってきませんか?
必要経費とも言えなくもないけど、うーん、というラインですね。
さらに、RAGで参照するドキュメント量が10倍になれば、この差額もそのまま10倍になります。
RAG側はそれこそ出力の質に影響するので、変に削ると品質に影響しますし。

※この削減率は一例です。実測では、小規模な表形式データ(3件×4フィールド)でJSON compactから約24%、Pretty JSONから約60%の削減を確認しています。データの構造や規模によって効果は変動します。

2. 爆速になる(推論速度の向上)

LLMはトークンを一つずつ生成します。
つまり、「生成するトークンが減れば、レスポンスは速くなる」
"name": "Alice", と書くのと、Alice と書くのでは、生成にかかる時間が物理的に違います。
リアルタイム性が求められるチャットボットやエージェント開発では、この数ミリ秒の積み重ねがUXに直結します。
離脱率とか結構馬鹿に出来ないんですよねぇ。

3. 「もっと覚えていられる」(コンテキストウィンドウの有効活用)

LLMが一度に扱える情報量(コンテキストウィンドウ)には限りがあります。
データフォーマットを軽量化することで、その分、より多くのプロンプト指示や、参考情報(Few-shot事例など)を詰め込むことができます
「トークンが足りなくてエラーが出た…」というあの絶望から、少しだけ解放されるかもしれません。

認知負荷の軽減も多少ありそうだよなあ、なんて思います。
実際に、「Lost in the Middle」(重要な情報がコンテキストの中間にあるとLLMが見落としやすくなる現象)という研究結果も報告されています。
ノイズ(無駄な記号)を減らして情報の密度を高めることは、LLMが「大事なこと」に集中するのを助ける効果も期待できそうです。

https://arxiv.org/abs/2307.03172


ライバルたちと比較。YAMLやCSVじゃダメなの?

ここで当然の疑問が湧きます。
「JSONが重いのはわかった。でも、軽量フォーマットならYAMLとかCSVもあるじゃん?」

その通りです。彼らも優秀な候補です。
なんならいつもお世話になってるシリーズだよ。
比較してみましょう。GOGO!

vs YAML

YAMLはJSONより括弧が少なくて読みやすいですが、「インデント」 という見えない敵がいます。
LLMにとってインデント(スペースの連続)は、意外とトークンを食いますし、生成時にインデントがズレてパースエラーになるリスクも高いです。
TOONは区切り文字が明確なので、構造崩れのリスクがYAMLより低い(と個人的には感じています)。
あと一般(非エンジニア)に引継ぎする時にYAMLファイルを触らせる事になるの不安で不安で仕方なかった(スペース1つで死ぬファイルと5000回くらい言った記憶)。

vs CSV

CSVは最強のトークン効率を誇ります。1,Alice,Admin。これ以上削れません。
実際、この記事の例(ユーザー3人のデータ)で測定したところ、TOONはCSVより約34%多くのトークンを消費しました(CSV: 29 tokens、TOON: 39 tokens)。
公式の大規模ベンチマークでは5-10%の差ですが、小規模データではこの差が大きくなる傾向があります。

では、なぜTOONなのか?

CSVには 「ヘッダーと値の対応が暗黙的」 という弱点があります。
列数が多くなると、LLMが「あれ、3番目のカラムって何だっけ?」と迷子になりやすく、ハルシネーション(幻覚)の原因になります。
特に、途中でヘッダーを参照し直す必要があるような長いデータでは問題が顕在化します。

TOONは [3,]{id,name,role}: という形式で、先頭でデータ件数とフィールド定義を明示します。
これにより:

  • 配列長の検証が可能(データの整合性チェック)
  • フィールド定義がデータと近い位置にある
  • LLMがデータ構造を把握しやすい

トークンは増えますが、その代償としてLLMの理解精度と信頼性が向上します。
純粋なトークン効率だけならCSVに軍配が上がりますが、TOONは「効率」と「構造の明確さ」のバランスを取った選択肢と言えます。


デメリットと限界

いいこと尽くめに聞こえますが、もちろん弱点もあります。
技術選定において「トレードオフ」を無視するのは危険ですからね。
もういつものシリーズ。

  • 深いネスト(階層構造)には不向き:
    • TOONは均一な配列データに最適化されています。YAMLライクなインデント構造でネストもサポートしていますが、深く複雑な階層構造の場合、JSONの方が効率的なケースがあります。公式ドキュメントでも「深くネストされた非均一な構造(tabular eligibility ≈ 0%)では、JSON-compactの方がトークンが少ない」とされています。
    • 個人的にSQLの深いネスト構造とは因縁があります。私もToonと同じ気持ちです、となった。
  • 型情報の曖昧さ:
    • JSONなら true (bool) と "true" (string) は別物ですが、TOON(テキスト表)にすると全部文字列に見えてしまうことがあります。厳密な型定義が必要な場合はスキーマ定義を別途渡すなどの工夫が必要です。これが場合によっては面倒な事になるな、と思いました。
  • 標準ライブラリがない:
    • import json して json.load() 一発、とはいきません。自前でパーサーを書くか、LLMにパースさせる必要があります。

実際の使いどころ:どこで使うのが正解?

既存のWeb APIのレスポンスを明日から全部TOONに変えろ!なんて暴論は言いません。
というかそんな工数どこから生えてくるんだ問題。
JSONのエコシステムは偉大です。

TOONが輝くのは、「LLMとの対話」 という閉じた世界の中です。

  • RAG (Retrieval-Augmented Generation):
    • 検索結果としてヒットした100件のドキュメント情報をLLMに渡すとき。JSONだと溢れるけど、TOONなら入る!というケースは多々ありえてきそうだな、と。
  • AI Agent間の通信:
    • Planner AgentがWorker Agentにタスクリストを渡すとき。機械同士の会話なら、丁寧なJSONより効率的なTOONの方が「話が早い」です。昨今の大エージェント時代を考えるとまあ徐々にAI Agentのやり取りに限ればToonにしようか、はあり得ていくのかも、と思った次第です。
    • 過去まだgeminiがスレッドの限界で、応答が不適切になった時に別のスレッドに移行する時に「人間への配慮はいらないから適切に内容を別スレッドに引き継ぐためのプロンプト出して」なんて指示をしていた事と同様に機械同士のやり取りを考えるとAIファーストじゃないですけど、人間を置いてけぼりにしても良いケースであれば良いものだと思います。
  • 構造化データの抽出:
    • LLMに「この文章からデータを抽出して」と頼むとき。出力フォーマットをTOON指定にすることで、生成速度を上げつつ、パース可能な形式で受け取れます。あくまでこれも抽出に限ったはなしですが。

でも、流行るかと言われると…? 現実的な運用ライン

ここまでTOONを褒めちぎってきましたが、「じゃあ明日からチーム開発でTOON導入しようぜ!」 となるかというと、正直難しいだろうな、と思ってます。今は。

以前書いた「数式×論理構造」でプロンプトを圧縮する話でも触れましたが、「AIにとって読みやすい形式」は、往々にして「人間にとって読みにくい(認知負荷が高い)形式」 です。

チーム開発において、JSONの「冗長だけど誰でも読める」というメリットは計り知れません。
誰でも読める、もまあ正直エンジニア層たちに甘えているというか、エンジニアたちは少なくともどこかしらでJSONと接触している率が高いので読める、という認識ではあります。

「これTOON形式だからパースしてね」とドキュメントに書くコスト、新メンバーへの学習コスト、デバッグのしにくさ…これらを天秤にかけると、多くのプロジェクトでは 「多少トークン代がかかっても、JSONの方が安全で楽」 という結論になるはずです。

既存のAPIシステムもJSONで動いていますし、それをひっくり返すのは現実的ではありません。

TOONが輝く「現実的な」ライン

なので、TOONの現実的な運用ラインは、以下のような 「局所的な最適化」 に落ち着くと思います。

  1. APIコストが事業の生命線になるレベルの大規模運用
  2. 個人開発や小規模チームで、意思決定のスピードが速い場合
  3. LLM内部の処理(ユーザーの目に触れない部分)でのデータ受け渡し

「JSONを置き換える次世代スタンダード」というよりは、「ここぞという時の必殺技(局所最適化ツール)」 として懐に忍ばせておく。
それくらいの距離感が、今のところちょうど良さそうな認識です。
そんなハイブリッドな使い分けができるのが、令和のエンジニアの処世術なのかもしれません。

参考リソース

Discussion

plusone|開発技法ノートplusone|開発技法ノート

TOON?なにそれ知らない、と思って調べたら最近発表されたフォーマットなんですね。
大量データをJSONに扱わせるのは荷が重いだろうね。
以前なら、CSVか固定長になるだろうけど、それぞれ一長一短がある。

で、まだ新しすぎてRFCにもなっていないみたいだけど、公式サイトでなくGithubのほうが詳しく乗ってるね。

LLMに仕様を正しく伝えないと、解釈間違えで正しい返答が期待できなくなるリスクがある。一回ハルシネーションが起きると、その修正にえらく時間がかかる。

でも、複数フォーマットを内包できるTOONはなかなか便利だと思う。

灯里(akari)灯里(akari)

plusoneさん~!コメントありがとうございます!!

TOON?なにそれ知らない、と思って調べたら最近発表されたフォーマットなんですね。

TOONはまだ生まれたてホヤホヤの概念で、RFC化どころか標準化するしないの動きもこれからという段階ですね~
Githubの方が詳しいのは本当にその通りで、私も調べながら「あれ?」ってなりました(笑)

LLMに仕様を正しく伝えないと、解釈間違えで正しい返答が期待できなくなるリスクがある。一回ハルシネーションが起きると、その修正にえらく時間がかかる。

特にパイプ区切りだと、値にパイプが含まれる場合のエスケープ処理とか、LLMが勝手に解釈を変えちゃうリスクとか…一度ハルシネーションが起きると修正に時間がかかるのは本当に身に染みます。

複数フォーマット内包の柔軟性は魅力ですよね!
ですが、まだまだ曖昧性との戦いも考えると標準化やもう少し浸透が進むまでは「ここぞという時の引き出し的知識」として慎重に使うのが良さそうかもと感じています。
今のところは「JSONで困ったときの別策」くらいの位置付けで、様子見しながら使っていくのが無難かもしれません~

kayakaya

vs csvの段落の内容が腑に落ちないのですが、こちらはどこの情報を参照しているのでしょうか?
視覚的に列が揃っているとLLMが理解しやすいというのは一体何故なのでしょうか?

CSVには 「ヘッダーと値の対応関係が遠い」 という弱点があります。
列数が多くなると、LLMが「あれ、3番目のカラムって何だっけ?」と迷子になりやすく、ハルシネーション(幻覚)の原因になります。
TOON(Markdownテーブル)は視覚的に列が揃っているため、LLMが構造を理解しやすく、CSVとJSONの「いいとこ取り」ができるバランスの良さがあります。

csvはjsonやtoonのようにデータに階層構造を持たせることができないからが理由なら比較として納得いくのですが、本当にtoonフォーマットはcsvフォーマットよりLLMが構造を理解しやすいのですか?

灯里(akari)灯里(akari)

kayaさん、コメントありがとうございます~!

  1. vs CSV段落について

ご指摘の通り、「視覚的に列が揃っているため、LLMが理解しやすく」という表現は紛らわしかったですね。

書き方が悪くて、「視覚的な揃い」がLLMの理解に直接影響するように読めてしまいました。
LLMはトークン列として処理するので、人間目線の「視覚的な揃い」は関係ないですよね~。

また、「本当にTOONはCSVよりLLMが構造を理解しやすいのか?」というご質問ですが、TOONは非常に新しいフォーマットで、現時点で査読付きの学術研究は見つかりませんでした(日英中で調べてきましたが)。

公式のベンチマークでは精度向上のデータがありますが、あくまで開発元の自社調査であり、第三者による検証はこれから、という領域です~。
記事では公式データを元に書きましたが、学術的な裏付けはまだない状況です。

記事では経験則ベースで書いてしまった部分もあるので、申し訳ありません。
私の書き方が悪かったです、ごめんなさい!

この部分は修正させてもらいました!

  1. TOONフォーマットについて

リポジトリ(https://github.com/toon-format/toon) のご確認と記載ありがとうございます!
記事内にも公式リポジトリへのリンクを追記させてもらいました。

こちらは完全に私のミスです…!

記事で「TOONの場合:」として示したMarkdownテーブル形式は、実際のTOONフォーマットではありませんでした。

正しいTOONは:

[3,]{id,name,role,status}:
1,Alice,Admin,Active
2,Bob,User,Inactive
3,Charlie,User,Active

という形式です。

執筆中にTOONの概念を咀嚼しながら「どう伝えるか」「どう書くか」を考えていて、その過程で出てきたMarkdownテーブル形式の例が、そのまま記事に残ってしまってました...。

該当箇所を修正させてもらいました…!

ご指摘ありがとうございました!大変助かりました~!