0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenAPIでAI生成APIの破壊的変更をCIで止める5チェック — oasdiff+Schemathesis実践レシピ

0
Posted at

更新日: 2026-07-22

AIにAPIの修正を頼むと、実装はほんまに速くなりました。

でも、customer_namenameへ変えただけで、別チームのフロントエンドやSDKが静かに壊れることがあります。変更した側のテストは緑。レビューコメントも自然。それでも利用者は動かない。

なんでだと思います?

「コードが動くか」と「すでに使っている人との約束を守ったか」は、別の問題やからです。

この記事では、OpenAPIをAPIの契約書としてGit管理し、AIが提案した変更を次の5チェックで止める方法を紹介します。

  1. OpenAPIそのものが正しいか
  2. 既存利用者を壊す差分がないか
  3. 実装がOpenAPIどおりに動くか
  4. 人が書かなかった境界値でも壊れないか
  5. 意図的な破壊的変更を誰が承認するか

僕はこの5つを「契約レッドライン」と呼びます。AIが提案したAPI変更を、構文・互換性・実装・探索テスト・人間承認の5境界で止めるCI設計です。

まずは全部入れなくて大丈夫です。10分で、APIのフィールド削除をCI相当の終了コード1で検知するところまで進めましょう。

最初に、3つの用語だけ

OpenAPI

OpenAPIは、HTTP APIのパス、入力、出力、ステータスコードなどを、機械が読める形で記述する標準です。公式仕様では、言語に依存しないHTTP APIのインターフェース記述として定義されています。文書はJSONまたはYAMLで表現できます。

たとえるなら、レストランの注文票です。「この名前の料理を、この形式で頼んだら、この形式で返す」という約束を書きます。

破壊的変更

破壊的変更、英語ではbreaking changeは、既存の利用者が修正なしでは動かなくなる変更です。

たとえば、レスポンスから必須フィールドを削除する、既存のパスを消す、受け付ける値を狭くする。変更した本人には小さく見えても、利用者には大きい。契約の片側だけを書き換える行為なんです。

契約テスト

契約テストは、実装が仕様という約束を守っているかを自動で確かめるテストです。

普通のユニットテストが「部品が動くか」を見るなら、契約テストは「部品同士の受け渡しが約束どおりか」を見ます。

人間、AI、決定的ツールの役割を先に分ける

ここ、かなり大事です。

担当 任せること 任せないこと
人間 互換性方針、利用者への通知、移行期限、例外承認 全差分の手作業チェック
AI 差分の要約、影響候補、互換な代替案、テスト草案 最終的な互換性判定、例外の承認
決定的ツール OpenAPI検証、差分検知、終了コード、再現可能なCI 業務上の損失や利用者との合意

AIに「この変更は安全?」と聞くと、もっともらしい説明は返ってきます。でも、同じ差分へ毎回同じ合否を返す保証はありません。

せやから、AIは考える補助。合否は決定的ツール。契約を変える責任は人間。この三分割が効きます。

10分の初回成功: 必須フィールド削除を止める

base.yamlを現在公開中の契約、revision.yamlをPRで提案された契約とします。revision側で、レスポンスの必須フィールドcustomer_nameを削除したとしましょう。

公式READMEにあるDocker利用法とbreakingコマンドを使うと、次の1本で比較できます。

docker run --rm -v "$PWD:/specs" tufin/oasdiff:1.17.0 \
  breaking --fail-on ERR \
  /specs/base.yaml /specs/revision.yaml

--fail-on ERRは、ERRレベルの破壊的変更が見つかったときに終了コード1を返します。CIは説明文を読む必要がありません。0なら通す、1なら止める。単純ですが、これが強い。

なお、この記事で記載したoasdiff v1.17.0は、2026-07-22に公式リポジトリで確認したリリースです。実運用では「常にlatest」ではなく、検証したバージョンまたはイメージdigestへ固定してください。

手元でDockerを使えない人向けに、仕組みだけを理解する最小Pythonも置いておきます。これはrequiredから削除されたフィールドだけを見る教材で、oasdiffの代替ではありません。

import json
import sys
from pathlib import Path

def required_fields(path: str) -> set[str]:
    spec = json.loads(Path(path).read_text(encoding="utf-8"))
    schema = spec["paths"]["/customers/{id}"]["get"]["responses"]["200"] \
        ["content"]["application/json"]["schema"]
    return set(schema.get("required", []))

base, revision = map(required_fields, sys.argv[1:3])
removed = sorted(base - revision)

if removed:
    print("BREAKING: removed required fields:", ", ".join(removed))
    raise SystemExit(1)

print("OK: required fields are backward-compatible")

このコードはダミーのOpenAPI JSONで実行し、customer_nameの削除を検知して終了コード1になることを確認済みです。限定された教材なので、パス削除、型変更、セキュリティ要件変更などはoasdiffへ任せてください。

チェック1: OpenAPIそのものをvalidateする

差分を見る前に、比較する契約書が壊れていたら話になりません。

oasdiffには、単一仕様を検査するvalidateコマンドがあります。公式READMEでは、不正な型、必須フィールド不足、不正な正規表現、解決できない$refなどの検査用途が案内されています。

docker run --rm -v "$PWD:/specs" tufin/oasdiff:1.17.0 \
  validate /specs/revision.yaml

ここでAIに任せるのは、エラー文を初心者向けに言い換え、修正案を作るところまで。仕様を勝手に直してコミットするところまでは任せません。requiredを消せばvalidation errorが消える場合でも、利用者との契約まで消してよいとは限らないからです。

もう一つ注意があります。OpenAPIの$refは別ファイルや外部URIを参照できます。所有不明の外部参照をCIが自由に取りに行くと、再現性が落ちるだけでなく、SSRFのようなリスクの入口にもなります。参照先をリポジトリ内か許可済みホストへ限定する。地味ですが大切です。

チェック2: baseとrevisionの破壊的変更を検知する

次がbreakingです。

ここで見るのは、ファイルが違うかではありません。利用者を壊す意味を持つ差分か、です。

説明文の変更は通常、既存クライアントを壊しません。一方、成功レスポンスの削除、必須パラメータの追加、受け付ける値の縮小などは影響が出ます。oasdiffはチェックごとに重要度を持ち、--fail-on ERR--fail-on WARNで停止ラインを選べます。

最初はデフォルトルールで始めて、誤検知が出たら「ツールが邪魔」と消すのではなく、どの契約を守りたいかをチームで言語化するのがおすすめです。

例外を許すなら、最低でも次の4点を残します。

  • なぜ互換な代替案が取れないか
  • 誰が影響を受けるか
  • いつまで移行期間を置くか
  • 失敗時にどう戻すか

例外に理由と期限がないと、レッドラインは穴だらけになります。

チェック3: 実装がOpenAPIどおりに返すか確かめる

ここで一回、反証を置きます。

baseとrevisionの差分が安全でも、実装が安全とは限りません。

仕様ではcustomer_nameを返すと書いてある。でも実装はnameを返している。OpenAPI同士を比べても同じなら、diffは何も言いません。契約書同士は一致しているけど、現場が契約書を守っていない状態です。

回避策は、起動中のAPIへ実際にリクエストし、レスポンスをschemaへ照合すること。ここでSchemathesisが使えます。

公式Quick Startでは、SchemathesisはOpenAPIまたはGraphQL schemaからproperty-basedな入力を生成し、ステータスコード整合、レスポンス検証、サーバーエラーなどを確認します。

uvx schemathesis run ./openapi.yaml \
  --url http://127.0.0.1:8000

このコマンドは自分が管理するローカルまたはテスト環境へ向けてください。所有していない外部APIや本番へ、生成した大量リクエストを無断で送らない。負荷試験でも脆弱性診断でも、許可と対象範囲が先です。

チェック4: 人が書かなかった境界値を探索する

手書きテストは、知っている失敗には強いです。でも、知らない失敗には弱い。

たとえば文字列なら、空文字、非常に長い文字、Unicode、想定外の組み合わせ。数値なら0、負数、境界の直前と直後。人間が全ケースを書くのはしんどいですよね。

property-based testingは、満たすべき性質を与え、ツールが多様な入力を作って反例を探す考え方です。SchemathesisはOpenAPIを入力生成の材料にします。

ただし、生成テストにも分からないことがあります。「在庫がない商品は409にすべき」「退会済み利用者へ請求してはいけない」といった業務ルールは、OpenAPIの型だけでは十分に表せません。

だから、生成テストで手書きテストを消すのではなく、役割を分けます。

  • schema整合と想定外入力: 生成テスト
  • 重要な業務シナリオ: 人間が設計した結合テスト
  • 失敗ケースの追加候補: AIに洗い出させる
  • 合否と受容損失: 人間が決める

チェック5: 意図的な破壊的変更を人間が承認する

破壊的変更は、全部が悪ではありません。

古いAPIを永遠に維持すると、認証方式の刷新や設計改善ができなくなります。大事なのは、機械が止めたあと「誰が、何を根拠に、どう移行するか」を人間が判断することです。

おすすめは、破壊的変更を隠して通すのではなく、次の順にすることです。

  1. 既存フィールドを残したまま新フィールドを追加する
  2. 古いフィールドをdeprecatedとして明示する
  3. 利用者と移行期限を合意する
  4. 観測で旧フィールド利用がなくなったことを確かめる
  5. 別PRで削除し、責任者が承認する

AIには移行案を作らせられます。でも「この利用者なら壊してよい」という承認は任せない。そこには契約、信頼、事業上の事情があるからです。

GitHub Actionsへ置く最小構成

5チェックを一度に入れると重いので、まずvalidateとbreaking diffをPRへ置きます。

name: api-contract

on:
  pull_request:
    paths:
      - "openapi/**"
      - ".github/workflows/api-contract.yml"

jobs:
  openapi-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Validate revision
        run: |
          docker run --rm -v "$PWD:/specs" tufin/oasdiff:1.17.0 \
            validate /specs/openapi/revision.yaml

      - name: Block breaking changes
        run: |
          docker run --rm -v "$PWD:/specs" tufin/oasdiff:1.17.0 \
            breaking --fail-on ERR \
            /specs/openapi/base.yaml \
            /specs/openapi/revision.yaml

GitHub Actionsではpull_requestpathsで、対象ファイルが変わったPRだけworkflowを起動できます。

このYAMLは構成例です。YAMLの構文は確認していますが、GitHub ActionsとDockerを組み合わせた実環境での動作は未確認です。実運用では、baseをリポジトリへ二重保存するのか、mainブランチから取り出すのか、リリース済みartifactを使うのかを決めてください。Docker imageもdigest固定を検討します。

AIに任せるプロンプト3本

1. API利用者向けに差分を説明する

次のOpenAPI差分を、API利用者への影響だけに絞って説明してください。

ルール:
- diffに書かれた事実と推測を分ける
- 合否判定はしない
- 不明点は「未確認」と書く

出力:
1. 変更
2. 影響しそうな利用者
3. 移行案
4. 未確認点

2. 互換な代替案を作る

この破壊的API変更を避ける代替案を3つ作ってください。

少なくとも次を検討してください:
- 既存フィールドを残して新フィールドを追加
- deprecated期間を置く
- v2 endpointを追加

各案に、実装負荷、利用者負荷、移行期限、撤去条件を付けてください。
最終選択は人間が行います。

3. 例外申請の抜けを探す

破壊的変更の例外申請をレビューしてください。

必須項目:
- 変更理由
- 影響する利用者
- 通知方法
- 移行期限
- ロールバック方法
- 承認責任者

欠けている項目と曖昧な根拠だけを指摘してください。
承認・却下はせず、人間へ残してください。

プロンプトは便利ですが、AIの「問題ありません」は品質ゲートではありません。一次情報、決定的な検査、人間の責任。この順番を崩さないのがコツです。

よくある失敗5つ

失敗 何が起きるか 回避策
OpenAPIが古い diffが現実を見ていない 実装からの生成または変更PRで同時更新
diffだけで安心する 実装のschema違反を見逃す 起動中APIへSchemathesisを実行
全警告を無視する ゲートが形骸化する ルール単位で理由と期限を残す
ツールをlatest固定する 日によってCI結果が変わる バージョンまたはdigest固定
AIに例外承認させる 業務上の損失を判断できない 責任者の人間承認を必須化

この方法が効かない条件

正直に言うと、OpenAPIで全ては守れません。

同じ200レスポンスでも、金額の単位が円から銭へ変わる。フィールド名も型も同じなのに、意味が変わる。認可の実装だけが抜ける。こういう変更は、schema diffだけでは捕まらない場合があります。

見分け方は、「型と必須/任意だけで、その業務上の約束を説明できるか」です。できないなら、代表シナリオの結合テスト、認可テスト、利用者レビューを追加します。

また、社内試作で利用者が1チームだけ、全員を同時更新できるなら、5段全部は過剰かもしれません。無料で十分な最小構成は、現在のOpenAPIを保存し、次の変更でdiffできる状態を作ること。まずそこだけでええんです。

今日10分でやる4ステップ

  1. 現在のOpenAPIをbase.yamlとして保存する
  2. 変更版をrevision.yamlとして保存する
  3. oasdiff breaking --fail-on ERRを実行する
  4. 終了コード1を見たら、その差分を消すのではなく、互換な代替案を1つ書く

成功条件は「OpenAPIを理解した」ではありません。自分の2ファイルを比較し、1件の破壊的変更を止められた。ここまでです。

AI時代って、コードを書く速度だけが資産になるわけではない気がします。速く変えられるからこそ、壊してはいけない約束を機械が読める形で残す。それが、明日のチームからの「あざっす」につながるんやと思います。

参考リンク(一次情報・2026-07-22確認)

生成AI活用エンジニア&3児のパパ。AI×開発の実践知を毎日発信しています → Xで次の実務レシピを受け取る

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?