破壊的変更に該当するものと、安全な出荷方法
1分で読めます 更新
破壊的変更とは、正しく書かれた呼び出し元が生き残れなかったであろう変更のことだ。この定義は重要だ。何かが「該当する」かどうかについての多くの議論は、実際には誰が正しくない持ち方をしていたかについての議論だからだ。呼び出し元があなたのドキュメントに従い、あなたの変更がそのコードを動かなくしたなら、その変更は破壊的だった。あなたが何を意図していたかは、これとは何の関係もない。
これがテストのすべてだ。この記事の残りは、そこから導かれるもの。何がテストに落ちるか、何が通るか、マージされる前に失敗をどう捕まえるか、そして自分が破壊的変更を出荷していると分かった時点で何をすべきかだ。
何が破壊的変更に該当するか
呼び出し元にテストを適用しよう。diffにではなく。文書化された動作だけに依存していた呼び出し元が、動作し続けるためにコード、設定、またはデータを変更しなければならないとき、その変更は破壊的だ。フィールドの削除、エンドポイントの改名、バリデーションの厳格化、デフォルト値の変更、値の型の変更、これらはすべて該当する。オプションのフィールドの追加は該当しない。バグの修正は通常該当しないが、下に一つ重要な例外がある。
| 変更 | 破壊的か | 理由 |
|---|---|---|
| フィールド、エンドポイント、フラグ、オプションの削除または改名 | はい | 正しい呼び出し元はそれを参照している |
| オプションのフィールドや新しいエンドポイントの追加 | いいえ | 既存の呼び出しは変わらない |
| オプションの入力を必須に変更する | はい | それを省略していた呼び出しは今や失敗する |
| 以前は受け入れていたバリデーションを厳格化する | はい | 動作していた入力が今や拒否される |
| デフォルト値の変更 | はい | それを設定していなかった呼び出し元は新しい動作を得る |
| 型の変更(文字列から数値へ、単一値から配列へ) | はい | 文書化された型のために書かれたパーサーが失敗する |
| オブジェクトのキーの順序を変更する | いいえ | 順序を文書化していない限り |
| 呼び出し元が依存していたバグを修正する | 実質的にはい | 偶発的な契約についてのセクションを参照 |
| レートリミットやサイズの上限を引き上げる | いいえ | 動作していたものは何も動かなくならない |
| レートリミットやサイズの上限を引き下げる | はい | 問題なかったトラフィックが今や制限される |
| エラーメッセージの言い回しを変更する | 場合による | それを文書化していたか、呼び出し元がそれに一致させている場合は破壊的 |
何が破壊的変更に該当しないか
以前動作していたすべての呼び出しが、変わらず動作し、同じ意味を持ち続けるなら、その変更は非破壊的だ。新しいエンドポイントの追加、オプションのリクエストパラメータの追加、レスポンスへのフィールドの追加、必須の入力をオプションにすること、上限の引き上げ、誰も照合していないエラーメッセージの改善は、いずれもテストを通過する。こうした追加的な変更は、通常のチェンジログ項目とともにマイナーリリースで出荷できる。
それでも追加的な変更が呼び出し元を壊す場面が三つある。未知のフィールドを拒否するデシリアライザを持つクライアントは、レスポンスに新しいフィールドが加わった最初の時点で失敗する。だから、認識できないフィールドは無視するよう、早い段階で文書化しておこう。新しいenum値は、網羅的なswitch文を持つすべての呼び出し元を壊す(詳しくは下で述べる)。そして、大きくなったレスポンスは、呼び出し元が考えたこともなかったサイズ上限、タイムアウト、列の幅を超えさせることがある。
表のうち四つの行は、より詳しく見る価値がある。意見の相違が生まれるのは、まさにそこだからだ。
チームが見過ごしがちな四つの破壊的変更
偶発的な契約。 あなたのAPIが三年間、同じ文書化されていないフィールドを返し続けているなら、呼び出し元はそれに依存して構築している。ハイラムの法則は短いバージョンだ。十分な数のユーザーがいれば、あなたのシステムの観察可能なあらゆる動作に、誰かが依存することになる。だからこそ「それはバグ修正だった」は弁護にならない。修正は正しくても、それでもなお破壊的でありうる。それを破壊的変更として出荷しよう。
スキーマの変更を伴わない動作の変更。 フィールドはまだそこにあり、型も同じだが、値が今や別の意味を持つようになる。以前はactiveかinactiveだったstatusが、今やsuspendedも返すようになると、網羅的なswitch文を持つすべての呼び出し元を壊す。ローカルタイムからUTCに移行するタイムスタンプは、ドキュメントを二度読んでいないすべての人を壊す。OpenAPIファイルのdiffには、これらのどれも現れない。
厳格化されたバリデーション。 TLDのないメール、末尾の空白、80文字を超える名前を拒否し始める。まさにそれを送っていたすべての呼び出し元は、先週まで動作していたリクエストで今や400を受け取る。バリデーションの変更は、「堅牢化」の修正として最もよく出荷されるものだ。
変更されたデフォルト値。 値を明示的に設定した誰も何も気づかない。それをしなかったすべての人、つまりほとんどの呼び出し元が、一行も変更せずに新しい動作を受け取る。変更されたデフォルト値は、まさにその設定を一度も見たことがないという理由で、あなたのユーザーの大半を壊す。
出荷前に破壊的変更をどう検出するか
プルリクエスト上のコントラクトとメインブランチ上のコントラクトをCIで比較し、破壊的な差分があればビルドを失敗させる。ほとんどのインターフェース形式にスキーマ差分ツールがあり、それぞれが自分の形式の破壊的変更のルールを知っている。
| インターフェース | ツール | 比較する対象 |
|---|---|---|
| REST(OpenAPI) | oasdiff | 二つのOpenAPI仕様。破壊的変更のレポート付き |
| gRPC(Protobuf) | buf breaking | .protoファイル。ワイヤレベルまたはソースレベル |
| GraphQL | GraphQL Inspector | 二つのスキーマ。破壊的変更と危険な変更を指摘する |
| Rustクレート | cargo-semver-checks | 公開APIと最後に公開されたバージョンの比較 |
| TypeScriptパッケージ | API Extractor | パッケージの公開APIのコミット済みレポート |
これらのツールは、削除されたフィールド、改名された操作、変更された型を確実に捕まえる。しかし、上の四種類のうち最初の二つ、偶発的な契約と動作の変更は見えない。どちらもスキーマには現れないからだ。明らかなものはツールで止め、残りは「正しい呼び出し元が気づくだろうか?」というレビューの問いで確かめよう。同じCIジョブは、CIでチェンジログ項目を必須にする方法で説明しているように、チェンジログ項目を必須にする場所としても自然だ。また、gRPCとProtobufのAPI変更では、ワイヤレベルのケースを取り上げている。
コミットで破壊的変更をどう示すか
Conventional Commitsでは、破壊的変更はコロンの前の!(feat(api)!: remove the legacy export endpoint)か、BREAKING CHANGE:で始まり説明が続くフッターで示す。どちらもメジャーバージョンに対応する。フッターは、誰が影響を受け、何をすべきかを明記した、チェンジログ項目の最初の下書きとして書こう。この規約でどこまで足りるかは、Conventional Commitsとチェンジログで扱っている。
同じルールはライブラリにも当てはまる。公開関数の削除、パラメータ型の狭め、戻り値の変更は、セマンティックバージョニングではメジャーバージョンだ。ライブラリは必ずしもそれに従うわけではない。Maven Centralの119,879件のアップグレードを調べた研究では、16.6%がセマンティックバージョニングに違反していたが、影響を受けたクライアントプロジェクトは7.9%にとどまった。そうした変更の大半が、どのクライアントも呼んでいないコードに触れていたからだ。破壊は、呼び出し元で測られる。
破壊的変更はどう出荷するか
日付とパスを添えて、公然と出荷する。以下のステップは順番通りであり、最後のものは多くのチームが飛ばすステップだ。影響を受けた人々に、彼らが待っていたことが今起きたと伝えることだ。
- それが該当するかどうかを決める。 diffではなく上記のテストを使おう。二人のエンジニアが同意しないなら、それは破壊的だ。その不一致こそが、呼び出し元が古い動作に合理的に依存していた可能性があるという証拠だ。
- バージョンを付ける。 セマンティックバージョニングの下では、破壊的変更はメジャーバージョンだ。日付付きまたはバージョン付きのAPIを運用しているなら、それは新しいバージョンに入り、古い方は宣言された日付まで動作し続ける。バージョンを付けられないなら、破壊的変更を出荷しているのではなく、チェンジログの項目付きの障害を出荷していることになる。どのスキームがバージョンを運ぶかはAPIバージョニングのベストプラクティスのテーマだ。
- コードがマージされる前に項目を書く。 その項目には固定された形がある。何が変わるか、誰に影響するか、何をすべきか、いつまでか。この四つすべてを埋められないなら、その変更はまだ準備できていない。リリースノートのテンプレートは、まさにこの理由から、こうした項目をバージョン番号ではなく日付付きで先頭に置く。
- リリース番号ではなく期限を伝える。 「v5で削除」は、あなたのリリースを追っていない人には何の意味もない。「2026年11月1日に動作しなくなる」はすべての人にとって同じ意味を持つ。
- 移行方法を提供する。 古い呼び出しの例を、新しいものと並べて示す。変更が改名であれば、両方の名前を同じ文で述べる。削除されたフィールドであれば、そのデータがどこに行ったかを伝える。
- 古い動作が文書化されていたすべての場所で告知する。 チェンジログ、エンドポイントを説明するドキュメントページ、SDKのリリースノート、そしてあるなら応答内の非推奨ヘッダー。一箇所だけで告知することは、たまたまそこを見た人にだけ告知することだ。
- ループを閉じる。 顧客がその変更を求めていたなら、あるいはそれにつながったバグを報告していたなら、それが出荷されたときに伝えよう。これは、それをユーザーに対して行われたことから、ユーザーとともに行われたことへと変えるステップだ。
破壊的変更の良い項目とはどんなものか
良い項目は、最初の行で影響を受ける呼び出し元を名指しし、日付を宣言し、修正方法を含む。以下は、私たちが使う形式での、厳格化されたバリデーションのケースの例だ。
ドメインのないメールアドレスは2026年11月1日から拒否されます。
POST /usersとPATCH /users/:idは現在、alice@localhostのような400 invalid_emailを返します。社内ディレクトリからユーザーを作成するあらゆる連携が影響を受けます。移行方法:完全修飾されたアドレスを送るか、フィールドを省略して後で設定してください。すでにドメインを持つアドレスであれば、変更は不要です。これは今年作成されたアカウントの99.4%に当てはまります。
この種の通知がどこに置かれるべきか、そして他に何がその隣にあるべきかについては、APIチェンジログで扱っている。
最後のパーセンテージは装飾ではない。それは読者に、心配すべきかどうかを伝える。それこそが、読者がその項目を開いたときの問いだったからだ。
なぜ単に避けないのか
代替案の方が悪いからだ。何も壊さないAPIは、これまでに犯したすべての間違いを蓄積していく。誤って名付けられたフィールド、間違ったデフォルト値、ローカルタイムのタイムスタンプ。それぞれが、午後の時間で移行できたであろう呼び出し元を守るために、あらゆる新しい呼び出し元に永遠に課される税金だ。安定性について最良の評判を持つチームは、めったに何かを壊さない。スケジュールに従い、移行パスと、対象となった人々に届いた警告とともに。
その警告の仕組みは、APIの非推奨化についての姉妹記事のテーマだ。それを告知する項目は、チェンジログのフィード内の他のどの項目とも同じ方法で作成される。マージされたpull requestから、人間のために保留され、その後、影響を受ける呼び出し元がすでに読んでいる場所で公開される。
FAQ
破壊的変更と非破壊的変更の違いは何か? 破壊的変更は、正しい呼び出し元に、動作し続けるためのコード、設定、またはデータの変更を強いる。非破壊的変更は、既存のすべての呼び出しを同じ意味のまま動作させ続ける。だから追加は通常安全で、削除、改名、厳格化されたルールは通常そうではない。
必須フィールドの追加は該当するか? はい。既存のすべての呼び出しはそれを省略しているため、既存のすべての呼び出しが今や失敗する。妥当なデフォルト値とともにオプションとして追加するか、エンドポイントをバージョン管理しよう。
バグ修正は該当するか? 該当しうる。呼び出し元がバグのある動作に依存していたなら、それを修正することはドキュメントが何と言っていようと彼らを壊す。誰も依存していなかったことを示せない限り、観察可能な出力を変えるあらゆる修正を破壊的として扱おう。
セマンティックバージョニングはウェブAPIに適用されるか? ルールは適用される。破壊的変更は新しいメジャーバージョンを取得し、古い方は宣言された期間動作し続ける。その番号は、パッケージバージョンではなく、URLや日付ヘッダーに存在することが多い。
どのくらいの事前通知で十分か? 呼び出し元が通知を見つけて作業をこなすのに十分な期間だ。公開APIでは90日が一般的な下限だ。エンドユーザーに出荷され、リモートで更新できないコードで使われるものについては、より長くすべきだ。
この記事の技術的な記述は第三者による確認を受けていません。誤りがあればお知らせください。修正します。