破壊的API変更の解説

破壊的API変更とは、APIの公開された契約に対する変更で、正しく旧契約に沿って書かれたクライアントを、クライアントが何も変更していないのに新契約で失敗させうるもののことです。この一文が理論のすべてであり、この記事の残りはすべてその帰結です。どの変更が該当するのか、無害に見えてそうではない有名な落とし穴、仕様の更新がデプロイされる前にどうレビューするのか、バージョン戦略がクライアントにどう時間を買うのか。2つの実際のドキュメントに適用するには、旧仕様と新仕様に OpenAPI Diff を実行し、このページの横で判定を読んでください。

契約とは未来への約束である

API仕様は気軽な意味でのドキュメントではありません。約束です。仕様が GET /pets は任意の limit パラメータを受け付け、文字列 name を含むオブジェクトで200を返すと宣言すると、すべてのクライアント作者はまさにその上に構築します。パラメータは不要なときに省略され、ステータスで分岐し、フィールドは文字列として読まれます。クライアントの正しさは約束に対して判断されるため、破壊的の定義は非対称です。正しく書かれた古いクライアントが失敗しうるなら、その変更は破壊的です。大半のクライアントがたまたま生き残ってもです。旧仕様から生成された厳格なSDKが、変更された型で失敗すれば十分です。壊れたのはクライアントではなく約束の方です。

この枠組みは、変更が「数えられる」かどうかについての議論のほとんどを解決します。エンドポイントの削除は破壊的です。約束にその存在が含まれていたからです。エンドポイントの追加は違います。古いクライアントは、自分が決して呼ばないパスについて何も約束していないからです。新しい任意のレスポンスフィールドは安全です。古いパーサは未知のフィールドを無視するからです。まともな形式では、追加だけの出力が読み手を無効にすることはありません。逆に、新しい必須入力は既存のすべてのリクエストに手を伸ばし、それを省略したものを無効にします。テストはいつも同じです。昨日の正当なトラフィックを明日のルールで再生し、何が失敗するか問うことです。

実務上重要な帰結が2つあります。第一に、契約とは公開されたものであり、サーバーが今日たまたま行うことではありません。トラフィックを嗅いでクライアントが発見した未文書化のフィールドは約束されておらず、それを削除するのは失礼ですが、厳密には破壊的ではありません。これは、保持するつもりのものだけを文書化すべきだという議論です。第二に、契約はそれが立つ標準の表面全体を含みます。RFC 9110 が固定したHTTPメソッドとステータスの意味論は約束の一部です。したがって、仕様ファイルがステータスの意味に一切触れていなくても、200や404の意味を再定義することは契約の変更です。

古典的な落とし穴

本番で最も一般的な破壊は必須フィールドの追加です。チームはリクエストボディにフィールドを追加し、新しいビジネスルールが必要だとして必須とマークし、出荷します。すると、そのフィールドを送らない既存のクライアントすべてが400を受け取り始めます。安全な版は退屈です。フィールドをサーバー側のデフォルト付きの任意として追加するか、新しいエンドポイントでのみ要求するかです。第2の落とし穴は列挙値の削除です。新しいUIが提供しなくなったため、ステータス値やカテゴリ、並び順が廃止されます。一方で、保存されたクライアントデータ、古いビルドのモバイルアプリ、パートナー統合は何年もそれを送り続けます。列挙値を受け入れ続けるコストはほぼゼロです。1つを撤回することは、エンドポイントの削除として扱うべきです。

型の変更は第3の落とし穴で、狭小化と拡大化の変装を伴います。狭小化——文字列から列挙へ、整数からより小さい範囲へ、自由なオブジェクトから固定された形へ——は、以前は有効だった入力を拒否し、一見して破壊的です。拡大化はより微妙です。レスポンスフィールドを文字列から文字列またはnullへ、整数のidを文字列のidへ変更することは寛大に見え、旧型用に構築されたすべての厳格なパーサと生成SDKを壊します。レスポンス側には鏡像の落とし穴があります。常に存在したフィールドが任意になることです。クライアントは無条件にそれを読みます。それを省略した最初のレスポンスは、誰か他の人のコードベースにおけるnullポインタです。

第4の落とし穴は意味論的で、どんな比較ツールも捕まえられません。フィールドは名前と型を保ち、意味を変えます。価格が静かにセントからドルへ、タイムスタンプがUTCからローカル時間へ、ソートされていたリストがされなくなる——仕様の差分は空で、すべてのクライアントが間違っています。意味論的な破壊こそ、機械的な差分がレビューの上限ではなく下限である理由です。誰も壊しえない変更を取り除き、人間の注意を、単に壊すかもしれない変更に集中させるためです。

デプロイ前にベンダーの仕様更新をレビューする

ベンダーは絶えず新しい仕様バージョンを公開し、「SDKを上げて再デプロイ」が静かな破壊が本番に届く経路です。規律あるレビューは15分で済みます。まず、両方のドキュメント——あなたのデプロイが今日実際に動いている仕様と、候補——を入手し、変更履歴を読むのではなく機械的に比較してください。変更履歴はマーケティング文書だからです。機能は列挙しますが、削除は列挙しません。OpenAPI Diff はそのリストをローカルで生成します。APIが内部のもので、その仕様が見知らぬ人のホスト型ツールに貼られるべきではない場合に重要です。

次に、破壊的リストをあなた自身の使用状況に対するチェックリストとして処理します。ベンダーの破壊の大半はあなたにとって理論上のものです。決して呼ばないエンドポイントの削除、決して作成しないリソースの新しい必須フィールド。危険な項目は、あなたのトラフィックと交わるもの——だからこそレビューには、差分だけでなく、あなたの呼び出し一覧が必要です。第3に、未文書化の表面を確認します。分岐するステータス、解析するエラーの形、読むヘッダー。webhookの消費者を守るのと同じ習慣——webhook署名の解説にあるように、ペイロード契約を固定してテストすること——は、他の誰かが管理するあらゆる契約に当てはまります。

最後に、段階的に導入します。ステージング用クライアントを新しいバージョンに向け、記録された本番リクエストを再生し(HARガイドが定める方法で扱われた、サニタイズ済みのキャプチャ)、どのユーザーよりも先にレスポンスを比較します。ベンダーが廃止期間やバージョンのピン留めを提供するなら、それを受け入れてください。最も安い破壊的変更は、ベンダーのリリース日ではなく、クライアントの準備が整った後、自分のスケジュールで採用するものです。

バージョン戦略:URL、ヘッダー、あるいは決して壊さない

破壊的変更への古典的な答えはバージョン識別子であり、古典的な議論はそれをどこに置くかです。URLバージョン(/v1/、/v2/)は明示的で、キャッシュに優しく、自明にルーティングでき、すべてのログ行に見えます。その代償は、API全体を一度にバージョン化するため、1つの破壊的なエンドポイントがすべてのクライアントを移行に引きずることです。ヘッダーバージョン——バージョンヘッダーや日付付きのコンテンツネゴシエーション方式——はURLをきれいに保ち、個々のリソースをバージョン化できますが、不可視という代償があります。バージョンはログ、リンク、ブラウザのバーから消え、デバッグは考古学になります。どちらも間違いではありません。業界は単にURLの代償をより進んで払います。午前3時には運用可能性が優雅さに勝つからです。

より深い戦略は、バージョンをめったに必要としないことです。追加のみの進化——決して削除せず、決して必須にせず、決して型を変えず、任意のものだけを追加する——は、単一のバージョンを無期限に存続させます。最も長命な公開APIが実際にそう運用しています。前もって規律が要ります。寛大な入力の許容、範囲が成長できるように初日から文字列として型付けされたid、拡張できるように設計された列挙、そして何かを撤回するずっと前に警告する廃止プロセスです。semver.orgに成文化されたセマンティックバージョニングは、OpenAPI Initiative自身の仕様が意図的に発行者に委ねている信号層を提供します。メジャーの引き上げは破壊を告げ、クライアントはそれに対してピン留めできます。

どの戦略を選んでも、仕組みに支えられた社会的契約としてのみ機能します。バージョンは宣言された期間、本当に並行して動かなければならず、廃止はクライアント作者が目にする場所(変更履歴、レスポンスヘッダー、大口消費者への直接通知)で告知されなければならず、旧バージョンの使用量は計測されなければならず、そうして「もう誰も使っていない」が希望ではなく事実になります。公開されても並行して運用できないバージョンは戦略ではありません。破壊の改名です。

比較ツールの居場所

機械的な比較ツールは2つの瞬間にその居場所を獲得します。レビューでは、「何か壊れたか?」を、並べて読む午後から、チェックを外していけるリストへ変えます。そして同じくらい有用に、否定を証明します。リリースが追加のみだと主張するとき、空の破壊的リストは感覚ではなく証拠です。継続的インテグレーションでは、同じ比較を仕様に触れるすべてのプルリクエストで実行し、プロジェクトの方針が要求するバージョンの引き上げや承認を伴わない破壊的変更が来たときにビルドを失敗させることができます。分類表が方針であり、ツールは締め切りのときにそれが忘れられることを拒むだけです。

その限界が、ワークフローの残りを定義します。比較ツールは宣言された契約だけを読みます。振る舞い、性能、意味論、クライアントが依存する未文書化の癖は見えず、病的に深いスキーマは永遠に展開するのではなく要約します。したがって完全なゲートはページではなくパイプラインです。契約には機械的な差分、振る舞いには実際のクライアント呼び出しを再生する契約テスト、意味論には段階的な展開、そして3つすべてが何かを見逃す日のための監視です。そのように使えば、差分は4層の中で最も安い層です。破壊がずっと仕様に書かれていたのに、誰もドキュメントを比較しなかったという、最も恥ずかしい種類の障害を消す15秒です。

それがこの記事が本当に主張する習慣です。仕様は保持するのが安く、比較するのはさらに安い。インシデントはそうではありません。所有または消費するすべての契約の、公開されたすべてのバージョンを保持し、出荷前にすべての候補を本番のバージョンと比較し、破壊的リストをリリースの本当の議題として扱ってください。そうするチームは、変更が誰かを壊す「はずだった」かどうかを議論しなくなります。出荷前から、日付付きでアーカイブされたリストを持っているからです。

出典と参考資料

よくある質問

必須フィールドの追加は常に破壊的ですか?

はい。既存のクライアントがすでに呼んでいるエンドポイントではそうです。昨日有効だったリクエストが今日拒否される、それが破壊的の定義です。標準的な逃げ道は、フィールドをサーバー側のデフォルト付きの任意として追加すること、新しいエンドポイントまたはバージョンでのみ要求すること、あるいは必須になる前にフィールドが受け入れられるが警告される廃止期間を告知することです。

URLバージョンとヘッダーバージョン、どちらが良いですか?

ほとんどのチームにはURLバージョンです。ログとリンクに見え、ルーティングとキャッシュが簡単で、それが何であるかに正直です。ヘッダーバージョンは、リソース単位の粒度が必要で、すでに強い可観測性を持つプラットフォームに合います。どちらも最悪の選択肢、つまりバージョンなしの破壊的変更よりましです。最良の戦略は追加のみの進化であり、バージョン化を日常的な税ではなく稀な出来事にします。

ツールはすべての破壊的変更を捕まえられますか?

いいえ。比較ツールは仕様が宣言するすべてを捕まえます。削除、新しい必須入力、型の変更、列挙の撤回です。意味論的な変更——同じフィールド、新しい意味——や、仕様が決して宣言しなかった振る舞いは捕まえられません。だからこそ機械的な差分は、契約テストや段階的な展開とともにパイプラインに属し、それ自体の台座には属しません。