HN 日本語サマリー

← 一覧へ戻る
プログラミング

SOML – 人間にとってのコンフィグフォーマット

SOML – A Config Format for Humans (github.com)

23 pointsby microflash10 コメント

要約

SOML(Sindre's Objectively Marvelous Language)は、人間が読み書きしやすい新しい設定ファイルフォーマットです。JSONのノイズ(引用符、カンマ、ブレース)を排除し、YAMLの曖昧さ(内容からの型推測)をなくし、構文から型を判断する厳密さと快適さを両立させています。コメントのサポート、日付や期間のネイティブな表現、エスケープ不要なパスや正規表現など、開発者の利便性を追求しています。

全文翻訳

[画像: SOML]

人間にとってのコンフィグフォーマット
SOMLはSindre's Objectively Marvelous Languageの略です。

SOMLは、書くのが快適で読みやすいコンフィグフォーマットです。トップレベルにはブレースは不要で、キーには引用符は不要、行末のカンマは改行に置き換えられ、コメントはほとんどどこにでも置くことができます。値の型は内容からではなく構文から決まるため、見たままが得られます:'123'は文字列、123は整数です。2026-09-19のような日付、2026-09-19T14:00:00Zのようなインスタント、30sのような期間は実際の値として扱われるため、誰もtimeout: 30の単位を推測する必要はありません。パスや正規表現はエスケープ不要で、プローズはブロック文字列に入ります。一つの正規化された形式と共有された適合性スイートにより、すべての実装が同じようにファイルを読み取ります。JSONは厳密さはありますが快適さはありません:いたるところに引用符、カンマ、ブレースがあり、コメントもありません。YAMLは快適さがありますが明確さはありません:-リストマーカー、ネストされたインデント、内容から推測される型があるため、country: noはfalseになり、version: 3.10は3.1になります。SOMLは、推測なしに、快適さと厳密さの両方を提供します。

警告

ドラフト仕様です。フィードバックを歓迎します。

name: 'api-gateway'
port: 8080 # ロードバランサーはこのポートを期待しています。
timeout: 30s
launch-date: 2027-09-19
windows-path: 'C:\Users\sindre\dev'
description:
	'''
	TLSを終端します。
	リクエストを内部サービスにルーティングします。
	'''
allowed-origins: [
	'https://example.com'
	'https://example.org'
]

同じ内容をJSONで表すと以下のようになります。

{
	"name": "api-gateway",
	"port": 8080,
	"timeout": 30,
	"launch-date": "2027-09-19",
	"windows-path": "C:\\Users\\sindre\\dev",
	"description": "Terminates TLS.\nRoutes requests to internal services.",
	"allowed-origins": [
		"https://example.com",
		"https://example.org"
	]
}

目標

SOMLは、人々が書き、読むファイルのためのデータフォーマットです。JSONからノイズを取り除き、コメントを追加したものです。JSONのオブジェクトと配列のツリー構造と厳密さを維持しつつ、リテラル文字列、ブロック文字列、日付、インスタント、期間を追加しています。

その設計は一つのルールに基づいています:値の型は内容からではなく、構文から決まる。'123'は文字列であり、123は整数です。検査によって値が再型付けされることは決してありません。

なぜSOMLなのか

  • JSONよりもノイズが少なく、コメントも可能。 キーには引用符が不要で、ダッシュが含まれていても構いません。行の区切りがカンマの代わりになり、トップレベルにはブレースが不要です。コメントは空白が許される場所ならどこにでも置けます。
  • 見た目が型を示す。 文字列には常に引用符があり、引用符のない値は数値、日付、インスタント、期間、またはキーワードでなければエラーとなります。したがって、country: noはfalseではなくエラーとなり、'10.20'と10.20はそれぞれ文字列と浮動小数点数として、書かれた通りに扱われます。
  • JSONが曖昧な箇所で厳密。 重複キー、単独のサロゲート、範囲外の数値は、各パーサーが決定する動作ではなくエラーとなります。整数は64ビットまで正確であり、3と3.0は異なる型として保持されます。
  • 書くのが快適。 リテラル文字列なので、パスや正規表現のエスケープが不要で、プローズのためにインデント解除されたブロック文字列があります。
  • 日付、インスタント、期間が値として扱われる。 2026-09-19はカレンダー日付、2026-09-19T14:00:00Zはインスタント、1h30mは期間であり、リーダーが文字列を解析したり単位を推測したりする必要はありません。
  • インデントは自由。 ブレースと改行が構造を決定するため、タブ、スペース、コピー&ペーストがファイルの意味を変えることはありません。ただし、ブロック文字列内ではインデントはテキストとして扱われます。
  • 一つの正規化された形式。 等しい値は等しいバイトを生成するため、ハッシュ化や署名に適しています。別のフォーマッターがコメント、メンバーの順序、値のスペルを保持します。そのルールは仕様の一部であり、すべての準拠フォーマッターは同じファイルに対して同じ出力を生成します。
  • 小さくテスト済み。 文法は先読みなしのワンパスであり、すべての実装は言語中立の適合性スイート(約1000ケース)を実行するため、すべての実装がファイルを同じように読み取ります。

例

/*
SOMLで表現できるすべてを一つのドキュメントにまとめました。ドキュメントはトップレベルの配列でも構いません。
*/

# キーはベアです。ダッシュも許可されているため、キーに引用符は不要です。
name: 'soml'
launch-date: 2027-09-19
released-at: 2026-09-19T14:00:00Z
'the name': '引用符は、キーにスペースが必要な場合の「エスケープハッチ」です'
404: '数字で始まるキーも可能です'

# 文字列:'...'はリテラル、"..."はエスケープされています。デフォルトは'を使用します。
regex: '^\d{4}-\d{2}-\d{2}$'
windows-path: 'C:\Users\sindre\dev'
shell: 'echo "$HOME" | tr -d "\n"'
escaped: "line one\nline two\t\"quoted\"\u{1f600}"

# ブロック文字列は、終了デリミタの列によってインデントが解除されます。
description:
	'''
	ブロック文字列は改行を保持します。
	各行の先頭のタブは削除されます。
	なぜなら、終了デリミタがタブ位置にあるからです。
	'''

# 数値:整数と浮動小数点数は異なる型です。
replicas: 3
ratio: 0.75
budget: 1_000_000
mask: 0b1010_1100
permissions: 0o644
color: 0xFF8800
flags: 0x00FF
limit: infinity
floor: -infinity

# ブーリアンとnull。
drained: false
owner: null

# 期間:h、m、s、ms、us、ns単位で正確です。
retry-after: 1m30s

# 配列。カンマまたは改行で項目を区切ります。
labels: ['prod', 'eu-west', 'canary']
matrix: [[1, 2], [3, 4]]
mixed: [1, 'two', true, null]

# オブジェクト。トップレベルのオブジェクトにはブレースは不要です。
pool: {
	min: 2
	max: 16
}

# 配列内のレコード。ブレースは必須であり、それは意図的です。
maintainers: [
	{name: 'Sindre Sorhus', email: 'sindresorhus@gmail.com'}
	{name: 'Random Person', email: 'person@example.com'}
]

# 空白が許される場所ならどこにでも、行コメントとブロックコメントがあります。
# インラインオブジェクトと一行のブレース付きドキュメントも機能します:
healthcheck: {path: '/health', interval: 10s, grace: 1.5s}

なぜYAMLではないのか?

SOMLは、YAMLの良い点(コメント、ファイル全体を囲むブレースなし、行末のカンマなし、引用符なしのキー)を維持しています。しかし、問題となる点(YAMLでは値の内容が型を決定する)を排除しています。SOMLでは構文が型を決定するため、'no'は文字列、3.10は浮動小数点数、2は整数となり、すべての実装で一貫しています。

SOMLが追加するもの:

  • ブロックコメント /* */(#に加えて)。
  • 30sや1h30mのような期間。これにより、timeout: 30で単位を推測する必要がなくなります。
  • 2026-09-19のような日付と2026-09-19T14:00:00Zのようなインスタント。YAML 1.2はタイムスタンプを削除しましたが、多くのパーサーは依然として読み取ります。そのため、YAMLの日付は文字列の場合もそうでない場合もあります。
  • 正規化された形式。等しい値はハッシュ化や署名のために等しいバイトを生成します。YAMLはスカラーに対してのみ定義しています。
  • 仕様の一部であるフォーマッター。コメント、キーの順序、値のスペルを保持します。YAMLはコメントをコンテンツではないとみなすため、ツールが削除する可能性があります。
  • 編集ルール。ツールは一つの値を変更し、残りのコメントとレイアウトを保持できます。
  • 通常、代わりに何を記述すべきかを示すエラーメッセージ。

SOMLが修正するもの:

  • 値が警告なしに型を変更する。 PyYAML(YAML 1.1)では、country: noはfalse、zip: 01234は8進数の668、time: 22:22は基数60の1342になります。YAML 1.2はこれらを修正しましたが、エコシステムの多くは依然として1.1に従っており、1.2でも推測が行われます:version: 3.10は浮動小数点数3.1になり、~、Null、空の値はnullになります。SOMLの文字列には常に引用符があるため、country: noはエラーになります。
  • キーが常に文字列であるとは限らない。 1: xはYAMLでは整数キーになり、PyYAMLはon: xをブーリアンtrueとして読み取ります。SOMLのキーは常に文字列であるため、404:はキー'404'になります。
  • インデントが構造を決定する。 スペースが一つ足りないだけで、キーが別のオブジェクトに移動し、ファイルは依然として解析されます。これがYAMLがそこでタブを禁止する理由です。SOMLはブレースと改行から構造を得るため、インデントはリーダーを助けるだけです。ただし、ブロック文字列内ではテキストとして扱われます。
  • ブロック文字列に改行が追加される。 YAMLのデフォルトの|は値の末尾に を保持しますが、SOMLはそうしません。
  • 大きな整数が精度を失う。 js-yamlはid: 9007199254740993を9007199254740992に変換します。SOMLの整数は正確なint64であり、それより大きい場合はエラーです。
  • 重複キーが見過ごされる。 a: 1の後にa: 2がある場合、PyYAMLは2を保持して何も言いません。仕様では重複は禁止されていますが、型解決後には1:と01:がスキーマによっては衝突する可能性があります。SOMLはデコードされた文字列を比較し、各重複を報告します。
  • 値がない場合はnullになる。 値のないtimeout:はYAMLではnullになり、コメントのみのファイルはPyYAMLではnullになります。SOMLは両方を拒否するため、忘れられた値がデフォルトとして通過することはありません。
  • 言語が大きい。 YAMLには、リストとマップの二つの構文、三つの改行ルールを持つ二つのブロック文字列スタイル、インデント指示子、アンカー、エイリアス、タグ、ファイルごとの複数ドキュメント、三つの処理ステージ、三つの推奨スキーマがあります。SOMLにはそれぞれ一つの構文、一つのブロック文字列スタイル、一つの型ルールがあります。
  • パーサーが意見の相違をする。 YAMLテストマトリックス(最新スナップショット2022)では、PyYAMLは402ケース中73ケースに失敗します。2024-01-01はPyYAMLでは日付、js-yaml 5では文字列であり、1_000はPyYAMLでは1000、js-yamlでは文字列です。SOML実装は、一つの適合性スイート(約1000ケース)を共有します。
  • 二つの機能がセキュリティリスクとなる。 エイリアスはYAML爆弾を可能にします:小さなファイルが数十億ノードに展開されます。タグはローダーがオブジェクトを構築することを可能にし、PyYAMLは4つのリモートコード実行CVE(評価9.8)を経験しました。safe_loadはオプトインであり、「safe」と名付けられたローダーでさえエイリアスをオンにすることができます。SOMLにはどちらもなく、ロード方法は一つで、ネストと範囲に制限があります。

なぜTOMLではないのか?

SOMLは、TOMLの良い点(予測可能な型、リテラル文字列、無視できるインデント、エラーとしての重複キー)を維持しています。しかし、問題となる点(キーの意味がそれ以前の内容に依存するテーブルルール、パーサーがサイレントに丸めたり、切り捨てたり、異なる方法で読み取ったりする値)を排除しています。SOMLファイルは、行がどこにあるか、どのパーサーがそれを読むかに関わらず、一つの意味を持ちます。

SOMLが追加するもの:

  • トップレベルの配列。TOMLファイルは常にテーブルであるため、レコードのリストにはラッパーキーが必要です。
  • ブロックコメント /* */(#に加えて)。
  • カンマの代わりに改行を使用するため、複数行の配列にはカンマが不要です。
  • 30sや1h30mのような期間。これにより、timeout = 30で単位を推測する必要がなくなります。
  • カレンダー日付とインスタントを、ローカル日時やローカル時間なしで、異なる型として保持します。
  • 正規化された形式。等しい値はハッシュ化や署名のために等しいバイトを生成します。
  • 仕様の一部であるフォーマッター。すべての準拠フォーマッターが同じ出力を生成します。

SOMLが修正するもの:

  • ネストの三つの方法。 [server]とport = 8080、server.port = 8080、server = {port = 8080}はすべて同じテーブルになり、それぞれ独自のルールがあります。プロジェクトではこれらを混在させることが多く、一つのファイル内でもそうです。SOMLには一つの方法しかありません:ブレースです。
  • 後の行が前のテーブルを変更する可能性がある。 ファイルの末尾でさえ、[[users]]は前の配列に追加され、[server.tls]は前のテーブルに追加され、ベアキーは最後のテーブルに追加されます。攻撃者が一つの値に改行を挿入できる場合、role = 'admin'を持つユーザーを追加できます。例えばCVE-2026-44244はコード実行のために.git/configに[core]セクションを挿入しました。BurntSushi/tomlでは、[auth]の下にある後のusers = ['mallory']が前の[auth.users.alice]テーブルをサイレントに置き換えることさえ可能です。SOMLでは、前のオブジェクトと配列は閉じられています。
  • テーブルヘッダーには隠れたスコープがある。 [server]は、ルートに戻る方法がないまま、timeout = 30でさえ、後続のすべてのキーをserverに移動させます。SOMLのブレースは境界を明示します。
  • ドット付きパスはヘッダーとは異なる動作をする。 [server]内では、logging.level = 'debug'はserver.logging.levelになりますが、[logging]はルートテーブルであり、server.port = 8080はserver.server.portになります。SOMLでは、キーはそれを囲むブレースに属します。
  • 拡張ルールはテーブルの作成方法に依存する。 [a.b]の後に[a]は有効ですが、a.b.c = 1の後に[a]は無効です。ヘッダーとドット付きキーのテーブルは拡張できますが、インラインテーブルはできません。また、[[servers]]はテーブルの配列に追加できますが、servers = []には追加できません。SOMLには暗黙のテーブルがなく、オブジェクトと配列にはそれぞれ一つの構文があります。
  • テーブルの配列は順序に依存する。 [servers.tls]は最も最近の[[servers]]にアタッチされるため、セクションを移動するとサイレントに別のサーバーにアタッチされる可能性があります。SOMLの配列要素は自己完結型のオブジェクトです。
  • ドット付きキーは構造をサイレントに変更する。 3.14 = 'pi'、example.com、tls.crtはネストされたキーであり、servers.0.portは配列要素ではなくキー'0'を作成します。SOMLではリテラルドットに引用符が必要です。
  • 名前の変更にはパスの追跡が必要。 [server.database]や[server.cache]のようなヘッダーは親名を繰り返し、リネームが一つ漏れるとサイレントに別のオブジェクトが作成されます。SOMLは親名を一度だけ名前付けします。
  • インデントの再調整が文字列を変更する。 複数行文字列はインデントと終了デリミタ前の改行を保持し、改行正規化はプラットフォーム依存です。SOMLはブロック文字列をインデント解除し、末尾の改行を追加せず、LFを要求します。
  • nullはないがnanはある。 TOMLは明示的なnullと欠落したキーを区別できないため、書き手はそれを省略するか失敗する必要があります:@iarna/tomlはnull値をサイレントに省略し、Pythonのtomlは配列内のNoneを文字列'None'に変換します。TOMLにはnanがあります。SOMLにはnullがあり、nanはありません。これは自分自身と等しくありません。
  • 数値と時刻がパーサー間で変更される。 1e999はPythonのtomllibとsmol-tomlでは無限大ですが、GoとRustではエラーです。1e-400はすべてでゼロです。.123456789秒はGoとRustでは正確ですが、tomllibでは.123456、smol-tomlでは.123です。9223372036854775808はtomllibでは機能しますが、Rustのtomlでは機能しません。SOMLの整数は正確なint64、インスタントはナノ秒を保持し、フィットしない浮動小数点数はエラーです。
  • 不正な入力が通過する。 TOMLは空またはコメントのみのファイルを空のテーブルとして読み込み、不正なUTF-8をU+FFFDで置き換えることを許可し、BOM処理を未指定のままにします。SOMLはこれらすべてを拒否するため、空になった設定はデフォルトにフォールバックできません。
  • JSONを書き直す必要がある。 TOMLは=を使用するため、JSONオブジェクトを値として貼り付けることさえできません。SOMLは:を使用するため、手書きのJSONは通常有効なSOMLであり、構文はJSON、YAML、JavaScriptからすでに馴染みのあるものです。

他のフォーマットとの比較

JSONは曖昧さはありませんが、書くのが煩雑です:ファイル全体をブレースで囲む必要があり、各項目にカンマが必要で、各キーに引用符が必要で、コメントがありません。YAMLは書くのが快適ですが、読むのが曖昧です。SOMLはJSONのデータモデルと厳密さを維持し、JSONが各実装に任せていることも決定します。YAMLからは、コメント、複数行文字列、ブレースやカンマのないトップレベルを取り入れますが、暗黙の型付け、意味を持つインデント、エイリアス、タグは取り入れません。TOMLからは、リテラル文字列と整数と浮動小数点数の分割を取り入れますが、トップレベルの配列を許可し、テーブルヘッダーやドット付きキーはありません。

SOMLJSONYAML 1.2TOML 1.1
コメント# および /* */なし##
トップレベルオブジェクトのブレース不要必要不要不要
別々の行での項目間のカンマ不要必要不要配列およびインラインテーブルで必要
引用符なしのキーはい、ダッシュと数字を含むいいえはいはい
値の型が構文から決まるはいはいいいえ、引用符のない値は内容によって型付けされるはい
整数と浮動小数点数を分離はいいいえはいはい
日付と期間日付、インスタント、期間いいえいいえ、ただし多くのパーサーは日付を読み取る四つの日付と時刻の型、期間なし
重複キーエラーパーサーに依存エラー、ただし一部のパーサーは最後のものを保持エラー
トップレベル配列はいはいはいいいえ
インデントが意味を変更するいいえいいえはいいいえ
正規化された形式はい別途スキーム(RFC 8785)のみスカラーのみいいえ
ライブラリ新しい、数言語どこにでもどこにでもほとんどの言語

JSON、JSON5、JSONC、TOML、YAML、その他のフォーマットとの完全な比較を参照してください。別のフォーマットがより適している場合についても記載されています。

仕様

フォーマットは仕様で定義されており、各決定の理由も示されています。FAQには、SOMLに期間や整数と浮動小数点数の分離型がある理由など、一般的な質問への短い回答があります。

実装

ツール

エディタプラグイン

アプリ

  • SOML Editor - SOMLファイルを閲覧・編集し、クイックルックをサポートするmacOSアプリ

Fun

  • soml-typescript-types - ランタイムコードゼロで、TypeScriptの型のみで書かれたSOMLパーサー、エンコーダー、正規化ツール

リンティング

追加のリンティングルールには、eslint-plugin-unicornと組み合わせてeslint-somlを使用してください。Unicornはrecommended-somlプリセットを提供しています。

ライセンス

仕様自体にはMITライセンスが適用されます。この仕様をライセンスを含めたり参照したりせずに自由に実装できます。