Claude CodeでSchemathesisを回し、OpenAPIと実装のずれを直させる
SchemathesisでOpenAPI定義から実APIをテストし、失敗コードごとに直す先を実装か仕様かに振り分けてClaude Codeに修正させる流れと、本番を叩かせない権限設計をまとめます。
OpenAPI定義と実装のずれは、定義を静的にlintしても見つかりません。定義の上では正しい書き方でも、実サーバーが定義にない404を返したり、必須のはずのフィールドを落としたりするからです。Schemathesisは、定義からリクエストを自動生成して実APIに送り、レスポンスが定義どおりかを検査するテストツールです。
この記事では、Schemathesisの失敗をClaude Codeに渡し、直す先を実装側か仕様側かに振り分けさせる流れを作ります。あわせて、テストが本番を叩かないようにする権限の組み方も扱います。定義そのものの書き方を見る静的な検証は、SpectralでOpenAPIをlintする構成が先にあります。ここで扱うのはその次の段、起動したサーバーに対する動的なテストです。
Schemathesisが見つけるずれの種類
Schemathesisは、OpenAPI(2.0・3.0・3.1・3.2)とGraphQLのスキーマからプロパティベースのテストを生成します。生成の入力は、スキーマ中の例、境界値、ランダムなデータです。各レスポンスは固定のチェック群で検査され、失敗にはST101のようなコードが付きます。
仕様と実装のずれに直結するのは、次のチェックです。
| チェック | 失敗コード | 見つけるずれ |
|---|---|---|
| not_a_server_error | 失敗コードST101 | 見つけるずれ5xxが返る |
| status_code_conformance | 失敗コードST103 | 見つけるずれ定義にないステータスコードが返る |
| response_schema_conformance | 失敗コードST108ほか | 見つけるずれレスポンスボディが定義のスキーマに違反する |
| negative_data_rejection | 失敗コードST201 | 見つけるずれ不正な入力が受理される |
| positive_data_acceptance | 失敗コードST202 | 見つけるずれ正しい入力が拒否される |
| ignored_auth | 失敗コードST401 | 見つけるずれ認証が宣言された操作に、認証なしでアクセスできる |
チェックはすべて既定で有効です。失敗ごとに、再現用のcurlコマンドとテストケースIDが出力されます。この「失敗コード」と「再現手段」が揃っている点が、Claude Codeに渡す素材として都合がよい部分です。何が壊れたかを文章で説明し直さなくても、コードを渡せば分類できます。
手元で最小構成を動かす
テスト対象は、手元で起動したローカルサーバーに固定します。ファイルで渡したスキーマでは、--urlでベースURLを指定する必要があります。
uvx schemathesis run ./openapi.yaml \
--url http://127.0.0.1:8000 \
--max-failures 20 \
--report ndjson \
--report-ndjson-path ./.contract/events.ndjsonuvxはuvパッケージマネージャーのコマンドで、Schemathesisを恒久インストールせずに隔離環境で実行します。直接インストールした場合はschemathesisまたはstに置き換えられます。--max-failuresは指定した件数の失敗で実行を止める上限です。Claude Codeの1回の修正サイクルに乗せる失敗を絞る目的で付けます。
NDJSONは、実行中のイベントを1行1JSONで書き出す形式です。失敗コードは各イベントのcodeフィールドに入ります。画面出力を読ませる代わりにこのファイルを読ませると、失敗の取りこぼしが減ります。
実行時間は、スキーマの大きさとサーバーの速さで変わります。全体の時間枠を決めたいときは--max-timeで秒数を渡せます。
失敗コードで直す先を振り分ける
失敗が出たら、最初に決めるのは「どちらが正しいのか」です。ここを曖昧にしたままClaude Codeに「テストを通して」と頼むと、実装を直すべき失敗まで仕様のほうを書き換えて通してしまう構成になりえます。コード別の見立てを、先にClaude Codeへ渡しておきます。
| 失敗コード | 実装を直す場合 | 仕様を直す場合 |
|---|---|---|
| ST101(5xx) | 実装を直す場合入力検証や例外処理の漏れ | 仕様を直す場合受理してはいけない入力に制約(minLengthなど)がない |
| ST103(未定義のステータス) | 実装を直す場合意図しない500などを返している | 仕様を直す場合実装が正しく返す404・403が定義に書かれていない |
| ST108(スキーマ違反) | 実装を直す場合必須フィールドの欠落・型の違い | 仕様を直す場合実際にnullを返す項目がnullableになっていない |
| ST201(不正入力の受理) | 実装を直す場合バリデーションが足りない | 仕様を直す場合制約が実装の意図より緩く書かれている |
| ST202(正しい入力の拒否) | 実装を直す場合実装が過剰に拒否している | 仕様を直す場合実装が課す制約が定義に書かれていない |
| ST401(認証なしで通る) | 実装を直す場合認証ミドルウェアの適用漏れ | 仕様を直す場合公開APIなのにsecurityが宣言されている |
この表は、各チェックの意味から導いた振り分けの目安です。唯一の正解ではありません。ビジネス上の意図は定義にも実装にも書かれていないことがあり、その場合は人が決めます。
コードの意味や典型的な原因を確かめるには、Schemathesisのexplainサブコマンドが使えます。
st explain ST103失敗コードまたはチェック名を渡すと、そのコードの意味、報告するチェック、よくある原因と直し方、許可や無効化の方法が表示されます。Claude Codeに「分からないコードはst explainで調べてから判断する」と指示しておけば、推測で直すことを減らせます。
CLAUDE.mdに振り分けのルールを書く
振り分けの方針は、セッションをまたいで効くようCLAUDE.mdに置きます。次はプロジェクトの規約として書く場合の例です。
## APIコントラクトテスト(Schemathesis)
- 実行は `./scripts/contract-test.sh` だけを使う。`st` や `schemathesis` を直接呼ばない
- 結果は `.contract/events.ndjson` を読む。失敗は `code` ごとに分類する
- 不明な失敗コードは `st explain <コード>` で意味を確認してから判断する
- 実装の不具合(ST101、ST201、ST401 など)は、実装を直す
- 実装が意図どおり動いていて定義が古い場合(ST103、ST108 など)は、
openapi.yaml の修正案を diff で示して止まる。自分で定義を書き換えない
- 修正後は `st replay <テストケースID>` で同じ入力を再送して確認する
- 判断がつかない失敗は、直さずに一覧で報告する仕様側の編集に「提案して止まる」という段階を挟んでいるのは意図的です。仕様はAPI利用者との約束なので、テストを通すための都合で書き換えてよいものではありません。実装の修正はgit diffで見直せますが、仕様の修正は外部に影響する変更になりえます。
本番を叩かせない権限設計
Schemathesisは、不正な入力、認証なしのリクエスト、定義にないメソッドまで、わざと送ります。生成されるリクエストを人が一つずつ選べないので、向き先を間違えると被害が出ます。Claude Codeに実行させるなら、向き先の固定を権限で強制します。
層を三つに分けます。
実行を専用スクリプトに集約する
接続先を固定した薄いスクリプトを用意します。
#!/bin/bash
# scripts/contract-test.sh
set -euo pipefail
mkdir -p .contract
uvx schemathesis run ./openapi.yaml \
--url http://127.0.0.1:8000 \
--max-failures 20 \
--report ndjson \
--report-ndjson-path ./.contract/events.ndjson引数を受け取らず、--urlをスクリプトの中に書いておくのがポイントです。Claude Codeが呼べるのはこのスクリプトだけにするため、接続先を変える余地が残りません。
直接の呼び出しを権限ルールで拒否する
.claude/settings.jsonに、スクリプトの許可と、Schemathesis本体の直接呼び出しの拒否を書きます。
{
"permissions": {
"allow": [
"Bash(./scripts/contract-test.sh)"
],
"deny": [
"Bash(st *)",
"Bash(schemathesis *)",
"Bash(uvx schemathesis *)"
]
}
}ルールの評価順は、拒否、確認、許可の順で、最初に合致したものが結果を決めます。拒否ルールは、より狭い許可ルールがあっても覆りません。スクリプトの内部で動くuvx schemathesisは、Claude Codeのツール呼び出しではないため、ここでは照合されません。
注意が要るのは、ラッパーです。timeoutやnohupなどの一部は照合前に取り除かれますが、npx・docker exec・direnv execのような実行ランナーは取り除かれません。docker exec api st run ...のような経路は、上のルールでは止まりません。ルールだけに頼らず、次の層を重ねます。
PreToolUse hookで接続先を検査する
PreToolUseのhookは、ツールの実行前に走り、tool_input.commandでコマンド文字列を受け取ります。終了コード2で終われば、標準エラーの内容がClaudeへのフィードバックとして返り、実行は止まります。hookのdenyはどの権限モードでも有効で、bypassPermissionsでも効きます。
#!/bin/bash
# .claude/hooks/guard-schemathesis.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -Eq '(schemathesis|(^|[[:space:]])st)[[:space:]]+(run|fuzz|replay)'; then
if ! echo "$COMMAND" | grep -Eq '(127\.0\.0\.1|localhost)'; then
echo "Blocked: Schemathesis は localhost 以外に向けて実行できません" >&2
exit 2
fi
fi
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-schemathesis.sh"
}
]
}
]
}
}文字列照合のhookは、抜け道を作りやすい仕組みです。上の例は「localhostの文字列がないst runを止める」だけの簡易な検査で、本番のURLを環境変数に隠した場合は見抜けません。スクリプトへの集約と合わせた補助線と考えてください。さらに堅く固めたいなら、Bashサンドボックスのネットワーク制限が使えます。サンドボックスのネットワークは、許可したドメインのリストが空の状態から始まり、リストにないホストへの通信は許可判断を経ます。本番のホストをリストに入れなければ、シェルから届かない構成にできます。
本番相当のデータに触れさせない
向き先を固定しても、ローカルのDBに本番のコピーが入っていれば、書き込みや削除のテストが実データに届きます。テスト用のDBを使い、テスト後に捨てられる状態にしておきます。
DELETEを除外しても、未定義のDELETEは送られる
破壊的な操作を避けようとして--exclude-method DELETEを付けるのは、よくある対策です。これは、スキーマに書かれたDELETE操作をテスト対象から外します。ところがSchemathesisには、スキーマに書かれていないメソッドをあえて送るカバレッジ段階があります。
phases.coverage.unexpected-methodsの既定値は、get・put・post・delete・options・patch・trace・queryです。定義にないメソッドを検査するunsupported_methodチェックは、405が返るべきところで200が返れば失敗にします。このチェックを無効にしても、報告をやめるだけで、リクエストは送られ続けます。リクエスト自体を止めるには、設定ファイルで送るメソッドを絞ります。
# schemathesis.toml
[phases.coverage]
# 未定義メソッドの送信を止める(ST204・ST205 の検査もできなくなる)
unexpected-methods = []共有のステージング環境のようにローカル以外で回す場合は、この設定が事故を防ぎます。代わりに、未定義メソッドへの誤った応答を検出する力は失われます。ローカルの使い捨て環境なら、既定のままで回すほうが多くのずれを拾えます。
直したあとの確認サイクル
修正後の確認は、失敗時に出力されたテストケースIDで行います。
st replay Xt9Kp2replayは、前回の失敗を再送します。失敗した入力はクラッシュファイルとして自動で保存され、ケースIDを省略すれば記録済みのすべてを再送します。直ったケースのファイルは既定で削除されるので、残ったものが「まだ壊れているもの」です。replayにもPreToolUse hookの検査は効きます。上のhookがreplayを照合対象に入れているのはそのためです。
一括で回し直す段階では、--seedに整数を渡すと同じ乱数列から同じテストケースを生成できます。修正の前後で入力を揃えて比較するときに使います。
すぐには直せない既知の失敗があるなら、--baselineが使えます。基準ファイルに記録された失敗は報告されますが、実行の失敗にはなりません。ファイルがなければ新規に作られるので、新しい失敗だけで落ちる運用に移せます。
つまずきやすい点
- ステートフルなチェックが動かない:
use_after_free(ST301)など、操作を順に呼ぶチェックは、OpenAPIのlinksなどで操作同士が結ばれているときだけ働きます。st runではステートフルの段階で、@schema.parametrize()のpytest連携では動きません - フォーマットの違反が大量に出る:
response_schema_conformanceのvalidate-formatsをfalseにすると、構造の違反に集中できます。ただし、日付形式などの違反を見逃す設定なので、一時的な切り分けに限ります - Claude Codeが定義を書き換えて通す: CLAUDE.mdに「定義の修正は提案で止まる」と書いても、Editの許可があれば書き換えられます。定義ファイルへの編集を確認制にしたいなら、
askルールでEdit(./openapi.yaml)を指定します - 認証ヘッダを
-Hで渡すとST401の判定が出ない: 明示したAuthorizationヘッダがスキーマの宣言と合わないとき、認証なしのリクエストを作れないため、そのチェックは判定を保留します
まとめ
Schemathesisは、失敗コードと再現手段が揃って出るため、AIに修正を任せる素材として扱いやすいテストです。決めておく点は二つあります。失敗ごとに直す先を実装か仕様かに振り分ける方針と、本番に向かないよう強制する仕組みです。前者はCLAUDE.mdに、後者はスクリプトへの集約、拒否ルール、hookの三層に書きます。
未定義メソッドが既定で送られる点は、見落としやすい事実です。破壊的な操作を除外した気になっても、unexpected-methodsを絞らなければDELETEは飛びます。ローカルの使い捨て環境以外で回すなら、最初にこの設定を確認してください。生成コードの扱いはOpenAPI Generatorを再生成で回す設定、定義の監査は42Crunchプラグインの手順と組み合わせると、定義から実装までを一巡で検査できます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- schemathesis.readthedocs.io/en/stable
- schemathesis.readthedocs.io/en/stable/quick-start
- schemathesis.readthedocs.io/en/stable/reference/cli
- schemathesis.readthedocs.io/en/stable/reference/checks
- schemathesis.readthedocs.io/en/stable/reference/configuration
- code.claude.com/docs/en/hooks-guide
- code.claude.com/docs/en/permissions
- code.claude.com/docs/en/sandboxing