Rustのプロジェクトを開けばCargo.tomlが見つかります。GitHubのリポジトリをクローンすれば、 YAMLファイルで溢れた.github/workflows/フォルダがあります。どちらのフォーマットも同じ 表面的な仕事をしています — 人間が編集する構造化された設定を保存する — しかし、まったく異なるトレードオフ をしています。YAMLが値を黙ってねじ曲げられた経験がある方、またはRustがパッケージマニフェストにYAMLでは なくTOMLを選んだ理由を疑問に思ったことがある方は、この記事がぴったりです。

本質的な違い:明示的 vs 暗黙的

TOMLとYAMLの根本的な違いは、パーサーがどれだけ推測することを許されるかについてです。 TOMLは明示的です:すべての値に 曖昧さのない型があります。文字列は常に引用符で囲まれます。ブール値は正確にtruefalseのみです。日時は独自の構文を持つファーストクラスの値です。暗黙的な型強制はありません — パーサーは賢くふるまおうとしません。

YAMLは反対方向に傾いています。 便利であろうとします:ほとんどの文字列に引用符は不要で、パーサーは値の見た目から型を推論します。 その推論が人々を罠にはめます。YAML 1.1仕様(今も多くのツールで使用されている)では、yesnoonofftruefalseがすべて ブール値として扱われます。1.0のような引用符なしのバージョン文字列は浮動小数点数として 扱われます。そして、ノルウェー問題があります。

YAMLの有名な落とし穴

ノルウェー問題はDevOpsコミュニティでミームのようになりました。YAML 1.1では、国コード NOがブール値falseとして解析されます。そのため、ISOの国コードを設定に マッピングする設定ファイルは、ノルウェーのエントリを黙ってブール値に変換してしまいます。 YAML 1.2仕様では これが修正されましたが、広く使われているパーサーの多く — 最近まではPyYAMLのデフォルトモードも含む — はまだYAML 1.1を対象にしています。信頼する前に、使用しているツールが実際にどのバージョンの仕様を 実装しているか確認してください。

ノルウェー問題の実際: YAML 1.1では、引用符なしのNOYesonoffはすべてブール値になります。修正は簡単です — 文字列を引用符で囲む — しかし問題はサイレントであることです。設定はエラーなくロードされ、文字列を 期待していた場所でブール値が得られます。
yaml
# YAML 1.1 implicit type coercion — all of these silently become booleans:
countries:
  norway: NO        # → false  ← The Norway Problem
  sweden: SE        # → "SE"   (fine, not in the boolean list)
  enabled: yes      # → true
  disabled: no      # → false
  feature_flag: on  # → true
  another: off      # → false

# Version strings can become numbers:
python_version: 3.10   # → float 3.1 (trailing zero dropped)
api_version: 1.0       # → float 1.0

# Safe: quote anything that could be ambiguous
python_version: "3.10"
country: "NO"
enabled: "yes"
  • 8進数リテラル。 YAML 1.1では01010ではなく8(8進数)として解析されます。0755のようなファイルパーミッション値では重要です。
  • タブ vs スペース。 YAMLはインデントにタブ文字を禁止しています。タブを使うように設定されたエディタからコードを貼り付けると、謎めいた解析エラーが発生するか、さらに悪いことにサイレントなずれが生じます。
  • 暗黙的なNull。 値のないキーはnullになります。手動で編集する際に誤って作成しやすいです。
  • インデントの感受性。 スペースが1つ多いと値がサイレントにある親から別の親に移動します。エラーなし、ただ間違ったデータ。

TOML:明示的な型が実際にどう見えるか

TOMLの型システムは TOML v1.0仕様に詳しく 記載されています。文字列は引用符(シングルまたはダブル)で囲む必要があります。ブール値は truefalseのみで、それ以外はありません。整数は整数です。浮動小数点数は 浮動小数点数です。そして日時 — JSONもYAMLもネイティブ型として持っていないもの — はTOMLでは ファーストクラスの値です。

toml
# TOML types are always unambiguous
name = "my-app"          # string — must be quoted
version = "1.0.0"        # string — quotes make it clear this is not a float
port = 8080              # integer
debug = false            # boolean — only true/false, nothing else
threshold = 0.95         # float

# Datetime is a first-class type — no string parsing needed
created_at = 2024-01-15T09:30:00Z
build_date = 2024-03-20

# Arrays
allowed_hosts = ["localhost", "staging.example.com", "api.example.com"]

# Inline tables
[database]
host = "postgres.internal"
port = 5432
name = "payments_prod"
pool_size = 20

TOMLでは国コードNOは引用符なしですが単なる文字列です。なぜなら、TOMLは 文字列のような値から型を推論しないからです — 引用符なしの値は厳格な構文規則に従います。文字列に なるには引用符が必要です。TOMLはYAMLの落とし穴を引き起こす暗黙的な型強制の仕組みを持っていません。

並べて比較:CIパイプライン設定

GitHubActionsワークフローを例に挙げましょう — これはYAMLのお得意領域です。GitHub Actionsが YAMLを期待しているためフォーマットが自然に合い、インデントベースのネストが論理的な構造を反映し、 コメントは明らかでないステップ設定を説明するために不可欠です。

yaml
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build --if-present

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

今度はTOMLで書いた同等のプロジェクトマニフェストを見てみましょう — ここでTOMLが輝きます。 実際のCargo.toml の構造と、同じ設定がYAMLではどう見えるかを比較してください:

toml
# Cargo.toml — Rust package manifest
[package]
name = "payments-service"
version = "2.4.1"
edition = "2021"
description = "Payment processing microservice"
license = "MIT"
authors = ["Alice Chen <[email protected]>"]

[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
tracing = "0.1"
anyhow = "1.0"

[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1

TOMLの弱点:テーブルの配列

TOMLにも荒削りな部分があります。テーブルの配列の構文 — [[double brackets]] — は仕様の中で最も紛らわしいものの一つです。JSONがオブジェクトの配列として書くものを表現する方法で、 最初は奇妙に読めます。

toml
# TOML array of tables — [[double brackets]] creates array entries
[[server]]
host = "web-01.example.com"
port = 443
region = "us-east-1"

[[server]]
host = "web-02.example.com"
port = 443
region = "us-west-2"

[[server]]
host = "web-03.example.com"
port = 443
region = "eu-west-1"

# The above is equivalent to this JSON:
# { "server": [
#   { "host": "web-01.example.com", "port": 443, "region": "us-east-1" },
#   { "host": "web-02.example.com", "port": 443, "region": "us-west-2" },
#   { "host": "web-03.example.com", "port": 443, "region": "eu-west-1" }
# ]}

YAMLでは同じ構造がより自然に読めます — インデントされたプロパティを持つリストだけです。 繰り返し配列エントリを持つ深くネストされたデータには、YAMLのインデントベースのネストはTOMLの [[section]]ヘッダーよりも真に冗長性が低いです。TOMLにはYAMLのアンカーとエイリアス システムに相当するものもないので、共有ブロックを一度定義してどこかで参照することができません。 複数のTOMLテーブルで同じ値セットを複製していることに気づいたら、手動でコピーするしかありません。

実際にどちらが優れているか

YAMLがデフォルトで優れているエコシステム:

  • GitHub Actions。 ワークフロー構文 全体がYAMLです。選択の余地はありませんが、それで問題ありません — インデントはジョブ、ステップ、 条件のネスト構造によく対応しています。
  • Kubernetes。すべてのマニフェスト — Deployments、Services、ConfigMaps、Ingressルール — はYAMLです。 Kubernetesオブジェクトモデル は深くネストされており、YAMLはそれをうまく扱います。
  • Docker Compose。サービス定義、ネットワーク、ボリューム — すべてYAMLです。ポートが 公開される理由や特定のヘルスチェックインターバルが使われる理由を説明するコメントはドキュメントの一部です。
  • Ansible。プレイブック、ロール、変数ファイル — YAMLで一貫しています。コメントのサポートは 明らかでないタスクパラメータを説明するために積極的に使われています。

フォーマットを制御できるときTOMLが優れている場合:

  • Rustプロジェクト。Cargo.tomlがゴールドスタンダードです。依存関係の 宣言、feature flags、ビルドプロファイル — すべてTOMLで。明示的な型は"1.0.0"のような バージョン文字列が文字列のままであることを意味します。
  • Pythonプロジェクト。pyproject.tomlはPythonプロジェクトのメタデータ、 ビルド設定、ツール設定(Black、isort、mypy、pytestすべてがここから読み込む)の標準になっています。
  • 曖昧さがバグを引き起こすツール設定。設定にバージョン文字列、国コード、またはブール値や 数値のように見える可能性のある値が含まれている場合、TOMLの明示的な型は解析の驚きのカテゴリ全体を 排除します。
  • フラットな設定。設定が主に1〜2レベルのネストのキーバリューペアである場合、TOMLは YAMLよりも読みやすく、JSONよりもクリーンです。

実践的な意思決定ガイド

ほとんどの場合、エコシステムが選択をしてくれます。GitHub ActionsはYAMLです。KubernetesはYAMLです。 RustはTOMLです。PythonのツールはTOMLです。実際に自由な選択がある場合は、これをガイドとして使ってください:

  • YAMLを使う:ツールが必須とするとき — CI/CDプラットフォーム、Kubernetes、Helmチャート、 Docker Compose、Ansible。慣習と戦うのは得より損が多いです。
  • YAMLを使う:複雑な設定をDRYに保つためにアンカーとエイリアスが必要なとき — TOMLには 同等のものがありません。
  • TOMLを使う:自分が制御するプロジェクトマニフェストやツール設定 — パッケージメタデータ、 リンター設定、ビルド設定。
  • TOMLを使う:設定にYAML 1.1が誤って解釈する可能性のある値 — バージョン文字列、国コード、 ブール値や数値のように見えるもの — が含まれているとき。
  • YAML文字列を引用符で囲む:ブール値、数値、またはnullと混同される可能性がある値は常に 引用符で囲む。特に:バージョン番号、国コード、数字で始まるもの、yesnoonoffなどの値。

両方のフォーマットを扱う

設定ファイルの検証や整形が必要ですか? TOMLフォーマッターがTOMLファイルを処理し、 YAMLフォーマッターがYAMLをカバーします。設定をあるフォーマットから 別のフォーマットに移行する必要がある場合 — たとえば、YAMLベースのツール設定をそれを好むプロジェクトの TOMLに変換する — TOML to JSONYAML to JSONの両方のコンバーターがJSONを出力します。そのJSONを 対象フォーマットに変換できます。中間ステップとしてJSONを経由するのが最も信頼性の高い方法である 場合があります。

知っておく価値があること:両フォーマットにはJSON互換のサブセットがあります。有効なJSONは有効な YAMLです(YAMLはJSONのスーパーセットです)。TOMLにはそのようなJSONとの関係はありませんが、 TOML仕様は意図的に シンプルに保たれています — 短い読み物です。完全な YAML 1.2仕様については 同じことは言えません。

まとめ

TOMLとYAMLは本当に競合していません。設計の優先事項に基づいて異なるニッチに落ち着いています。 YAMLの暗黙的な型とインデントベースの構造は、チームが管理する大規模なネスト設定 — Kubernetesマニフェストや GitHub Actionsワークフローなど — の自然なフィットにします。TOMLの明示的な型とフラットなセクション構造は、 バージョン文字列や国コードの誤読が実際のバグを引き起こすプロジェクトマニフェストやツール設定の自然な フィットにします。

覚えておくべき一つのこと:YAMLを書いているとき、ブール値、数値、またはnullのように見える値がある場合 — それらを引用符で囲む。これはYAML関連の設定バグのほとんどを防ぐ唯一の習慣です。そして、新しいプロジェクトを 始めて設定フォーマットを自由に選べる場合は、TOMLを試してみる価値があります。 TOMLのGitHubリポジトリ には良い例があり、Cargo.tomlpyproject.tomlを書いたことがあれば、 フォーマットの魅力はかなり明確になります。