Codex use case
バリデーションモジュールの移行
1 つのモジュールをアップグレードし、呼び出し側から見た動作が変わらないことをテストで示します。
Codex に、移行範囲を明記した仕様書、契約テスト用のフィクスチャ、セットアップ済みのクラウド環境を渡します。Codex は対象のバリデーションモジュールをアップグレードし、その範囲に絞った差分、移行前後の動作を照合した結果、残っている相違点、ロールバック手順を返します。
Best for
- レガシーなバリデーションコンポーネントを移行する開発チーム
- 契約が明文化され、テストを繰り返し実行できる移行作業
- 他のモジュールの動作を維持しながら、小規模な変更をレビューして取り込みたいチーム
Contents
バリデーションモジュールの移行
1 つのモジュールをアップグレードし、呼び出し側から見た動作が変わらないことをテストで示します。
Codex に、移行範囲を明記した仕様書、契約テスト用のフィクスチャ、セットアップ済みのクラウド環境を渡します。Codex は対象のバリデーションモジュールをアップグレードし、その範囲に絞った差分、移行前後の動作を照合した結果、残っている相違点、ロールバック手順を返します。
Codex に、移行範囲を明記した仕様書、契約テスト用のフィクスチャ、セットアップ済みのクラウド環境を渡します。Codex は対象のバリデーションモジュールをアップグレードし、その範囲に絞った差分、移行前後の動作を照合した結果、残っている相違点、ロールバック手順を返します。
Related links
Best for
- レガシーなバリデーションコンポーネントを移行する開発チーム
- 契約が明文化され、テストを繰り返し実行できる移行作業
- 他のモジュールの動作を維持しながら、小規模な変更をレビューして取り込みたいチーム
Starter prompt
このワークフローの適用条件
移行の対象範囲が明確で、移行前後の動作を比較できるテストがある場合に、このワークフローを使います。Codex には、移行の概要、対象モジュールとその呼び出し元、契約テスト用に固定したフィクスチャ、セットアップ済みのクラウド環境を用意します。依頼する変更は 1 件に絞り、検証記録とロールバック手順も求めます。
このサンプルでは、注文サービスのバリデーションモデルを 1 つ、Pydantic v1 から v2 に移行します。元の実装、互換性を確保するための準備段階、移行後のモジュールを収録しています。テストでは、実際の FastAPI ルーター、エラーハンドラー、レスポンスのシリアライズ、隣接するレガシールート、公開 OpenAPI ドキュメントを検証します。データベースの移行やデプロイは対象外です。
クラウド環境の準備
-
作業場所 > クラウドの順に選び、公開済みの環境を選択します。
-
環境を新しく作る場合は、環境を作成を選び、使用する GitHub リポジトリを選択します。ランタイム、依存関係、隔離されたテスト用サービスの準備を Codex に依頼します。
-
セットアップレポートを確認し、失敗したチェックの原因を解消します。新しいタスクを開始する前に、公開を選択します。
依存関係やサービスのセットアップが変わった場合は、再利用する環境を編集し、テストしてから再公開を選択します。
更新後の環境は新しいタスクで検証します。リポジトリを更新しても依存関係のキャッシュは保持され、インストールコマンドや起動コマンドは再実行されません。既存のタスクはそれぞれの状態を保持します。
必要なネットワーク接続先と認証情報は別途設定します。
各段階で、環境とセットアップを特定できる参照情報、ソースのコミット、Python のパッチバージョン、フレームワークと依存パッケージのバージョン、パッケージマネージャーのバージョン、ロックファイルのハッシュを記録します。新しいタスクの結果を使う前に、そのタスク内で各バージョンを確認します。
サンプルの実行
移行サンプル一式をダウンロードします。独立した 3 つの uv プロジェクトと、各プロジェクトの依存関係を解決済みの uv.lock ファイル、アプリケーションの実装、各段階で共通のエラーおよびスキーマ用アダプター、固定した HTTP フィクスチャ、契約テストランナーが含まれています。
Python 3.11、3.12、3.13 のいずれかを使用します。uv をインストールし、次のコマンドを実行します。
unzip sample.zip
cd migrate-a-validation-module
uv run --project support/target --python python3 --frozen python run.py --compare-stages --output evidence
比較を実行すると、段階ごとに個別の .venv が作成され、uv sync --frozen で依存パッケージがインストールされます。どの段階でも、ランナーを起動した Python インタープリターを使用します。HTTP フィクスチャとスキーマフィクスチャは全段階で共通です。依存パッケージのバージョンはロックファイルの指定どおりです。
レガシーの Pydantic v1 互換ブリッジが対応していないため、Python 3.14 以降は対象外です。実行が成功すると、最後に次のメッセージが表示されます。
PASS: legacy, prepared, target HTTP/schema/serialization contracts match
Native model schemas differ; the existing public schema adapter is preserved.
evidence/legacy.json、prepared.json、target.json、summary.json を確認します。実際のレスポンス、ランタイムのバージョン、ソース・フィクスチャ・ロックファイルそれぞれのハッシュ、実行したコマンド、終了コード、元のログのパスが記録されています。
ランナーは、FastAPI の TestClient を使い、サンプルアプリケーションの HTTP エンドポイントを同一プロセス内でテストします。選択したクラウドタスク内で実行し、そのタスクの検証記録を保存してください。
python3 run.py を --compare-stages なしで実行するのは、Pydantic v2 をインストール済みの環境で移行後のモデルだけを確認する場合に限ります。この単体実行モードでは、バリデーションとシリアライズに関する 7 ケースを検証します。HTTP の検証や段階間の比較は行いません。移行を検証するには、上記の比較コマンドを実行してください。
比較コマンドでは、各段階で 23 件の HTTP テストケース、公開 OpenAPI ドキュメント、レスポンスのシリアライズを検証します。ローカルでは Python 3.11、3.12、3.13 のすべてで合格しました。検証済みのバージョンと実行履歴は、ダウンロードしたサンプルの README.md に記載されています。使用するクラウド環境でも現在の比較コマンドを実行し、その環境での結果を記録してください。
段階ごとの依存関係と移行範囲
| 段階 | Python / FastAPI / Pydantic |
移行範囲 |
|---|---|---|
| 移行前 | 3.11–3.13 / 0.119.0 / 1.10.22 |
既存のモデルは pydantic からインポートします。 |
| 準備後のベースライン | 3.11–3.13 / 0.119.0 / 2.12.3 |
両方のモデルが pydantic.v1 を使用します。同梱されている v1 の実際のバージョンを記録します。 |
| 移行後 | 3.11–3.13 / 0.119.0 / 2.12.3 |
v2 に移行するのは OrderInput のみです。隣接するルートは引き続き専用の v1 モデルを使用します。 |
準備後と移行後の依存関係のロックファイルは同一です。モジュールの移行で変更するのはモデルとそのシリアライズメソッドだけで、依存関係の構成は変わりません。
準備段階の変更は、モデルの移行とは別にレビューします。この段階では、v1 のまま残すルートのアダプターも変更します。レスポンスの検証前に dict(by_alias=True) を呼び、エイリアスをキーとする辞書を返すようにします。v2 環境で v1 モデルを直接返すと、公開 API のエイリアスが失われることがあります。
移行後の注文用アダプターでは model_dump(by_alias=True) を使用します。隣接する v1 ルートでは、準備段階で導入したアダプターをそのまま使います。
FastAPI 移行ガイドによると、v1 と v2 のルートの混在が一時的にサポートされていたのは 0.119.0 から 0.127.x までです。0.128.0 でこのサポートは削除されました。
このサンプルでは移行過程を示すため、意図的に 0.119.0 に固定しています。新しいバージョンの FastAPI を使うアプリケーションでは、移行範囲内をすべて v2 で統一してください。一時的な互換ブリッジには担当者を置き、削除する条件を明確にします。v1 モデルと v2 モデルを入れ子にしないでください。このブリッジは Python 3.14 への移行には使えません。
契約テストの確認
固定した HTTP テストケースでは、正常なリクエスト、備考の省略と明示的な null 指定、ID の欠落と不正な ID、複数の未知のフィールド、複数種類のフィールドエラーの混在、不正な備考、不正な形式の JSON、誤ったコンテンツタイプ、ボディの欠落、エイリアス、配列や文字列のボディ、隣接するレガシールートを検証します。比較するのは、ステータスコード、Content-Type ヘッダー、パース後の JSON レスポンス全体です。エラーフィールドを取り除いたり、期待する型を変更したりすることはありません。
このサービスには、移行前からアプリケーション側で定義しているエラーレスポンスの形式と公開 OpenAPI ドキュメントがあります。エラー用とスキーマ用のアダプターはそのまま残します。移行後もエラーの順序は従来どおりです。宣言済みフィールドのエラーが先に並び、その後に未知のキーがアルファベット順に並びます。
テストでは、error.fields 配列全体を要素の順序も含めて比較します。実際のレスポンスを並べ替えることはありません。ランナーは、各モデルが生成するネイティブスキーマを個別に記録し、それらに差があることも確認します。公開スキーマが一致していても、依存ライブラリが生成するスキーマまで同一とは限りません。
サービスは入力に関する従来の動作を維持します。[["order_id", "ord_123"]] のようなキーと値のペアの配列は、オブジェクトとして受け付けます。[] と空の JSON 文字列はいずれも空のオブジェクトとして扱い、ID 欠落エラーを返します。空でない文字列は引き続き不正なボディとして扱います。移行後のモデルでも、dict(value) で変換できる入力は受け付けます。入力をオブジェクトだけに制限するには、契約を変更するという担当者の判断が必要です。テストの期待値を書き換えるだけでは済みません。
自分のリポジトリで移行する際は、コードを編集する前に、現行実装が実際に返すレスポンスをフィクスチャとして固定してください。省略可能なフィールドはデフォルト値を明示して維持し、バリデーションの厳密さと未知のフィールドの扱いも保持します。実際のエラーハンドラーとレスポンスのシリアライズをテストしてください。
ライブラリが直接返すバリデーションエラーはバージョンによって異なります。Pydantic 移行ガイドでは、エイリアス、暗黙の型変換、省略可能なフィールド、カスタムバリデーション関数の変更点も説明しています。v2 では、これらの関数内で発生した TypeError は ValidationError に変換されません。
契約テストランナーの確認
以下は、ダウンロードしたサンプルの run.py からの抜粋です。実行には ZIP を展開し、収録されているファイル一式を使用してください。各段階の依存関係を固定したプロジェクトと HTTP フィクスチャも含まれています。
3 段階の比較
class OrderInput(BaseModel):
internal_order_id: StrictStr = Field(alias="order_id")
customer_note: StrictStr | None = None
model_config = ConfigDict(extra="forbid", populate_by_name=False)
@model_validator(mode="before")
@classmethod
def preserve_legacy_mapping(cls, value):
# v1 tries dict(value), so pairs, [] and "" can become objects.
# Preserve successful conversions without changing failed-input errors.
if not isinstance(value, dict):
try:
return dict(value)
except (TypeError, ValueError):
return value
return value
def serialize_order(payload):
return OrderInput(**payload).model_dump(by_alias=True)
def preserve_legacy_error_order(fields):
# v1 reports declared-field errors first and unknown keys alphabetically.
# v2 keeps unknown keys in input order; preserve the existing HTTP contract.
known = [field for field in fields if field["code"] != "extra_forbidden"]
extra = sorted(
(field for field in fields if field["code"] == "extra_forbidden"),
key=lambda field: field["path"],
)
return known + extra
def check_target_model():
for payload, expected in (
({"order_id": "ord_123"}, {"order_id": "ord_123", "customer_note": None}),
(
{"order_id": "ord_123", "customer_note": "Leave at desk"},
{"order_id": "ord_123", "customer_note": "Leave at desk"},
),
(
{"order_id": "ord_123", "customer_note": None},
{"order_id": "ord_123", "customer_note": None},
),
):
assert serialize_order(payload) == expected
for payload, expected in (
({}, [("order_id", "missing")]),
({"order_id": 123}, [("order_id", "string_type")]),
(
{"order_id": "ord_123", "customer_note": 17},
[("customer_note", "string_type")],
),
(
{"order_id": "ord_123", "unexpected": True},
[("unexpected", "extra_forbidden")],
),
):
try:
OrderInput(**payload)
except ValidationError as error:
assert [
(item["loc"][0], item["type"]) for item in error.errors()
] == expected
else:
raise AssertionError(f"Accepted invalid order: {payload}")
print("PASS: target model validation and serialization (7 cases)")
print("Model-only checks; use --compare-stages for HTTP migration parity.")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=Path, default=ROOT / "evidence")
parser.add_argument("--compare-stages", action="store_true")
args = parser.parse_args()
if not args.compare_stages:
check_target_model()
return
args.output.mkdir(parents=True, exist_ok=True)
reports = []
commands = []
for stage in ("legacy", "prepared", "target"):
project = Path("support") / stage
stage_commands = [
[
"uv",
"sync",
"--project",
str(project),
"--python",
sys.executable,
"--frozen",
],
[
"uv",
"run",
"--project",
str(project),
"--python",
sys.executable,
"--frozen",
"python",
"test/run_stage.py",
stage,
str(args.output / f"{stage}.json"),
],
]
for command in stage_commands:
completed = subprocess.run(
command, cwd=ROOT, capture_output=True, text=True
)
label = f"{stage}-{len(commands)}"
(args.output / f"{label}.log").write_text(
completed.stdout + completed.stderr
)
commands.append(
{
"command": command,
"exit_code": completed.returncode,
"log": f"{label}.log",
}
)
(args.output / "commands.json").write_text(
json.dumps(commands, indent=2, sort_keys=True) + "\n"
)
if completed.returncode:
print(completed.stdout + completed.stderr, file=sys.stderr)
raise SystemExit(completed.returncode)
print(completed.stdout, end="")
reports.append(json.loads((args.output / f"{stage}.json").read_text()))
assert all(report["python"] == platform.python_version() for report in reports)
for report in reports[1:]:
for key in (
"observations",
"public_openapi",
"direct_call",
"fixture_sha256",
"schema_fixture_sha256",
):
assert report[key] == reports[0][key], f"Parity failed: {key}"
assert reports[1]["lock_sha256"] == reports[2]["lock_sha256"]
native_schema_differs = (
reports[0]["native_model_schema"] != reports[2]["native_model_schema"]
)
assert native_schema_differs, "The example should expose native schema drift"
manifest = {
str(path.relative_to(ROOT)): digest(path)
for path in sorted(ROOT.rglob("*"))
if path.is_file()
and not path.resolve().is_relative_to(args.output.resolve())
and not any(
part in {".venv", "__pycache__", "evidence", ".ruff_cache"}
for part in path.relative_to(ROOT).parts
)
}
summary = {
"status": "pass",
"scope": "synthetic in-process FastAPI TestClient execution",
"stages": [report["stage"] for report in reports],
"http_cases_per_stage": reports[0]["http_cases"],
"native_model_schema_differs": native_schema_differs,
"public_http_and_schema_parity": True,
"commands": commands,
"source_sha256": manifest,
}
(args.output / "summary.json").write_text(
json.dumps(summary, indent=2, sort_keys=True) + "\n"
)
print("PASS: legacy, prepared, target HTTP/schema/serialization contracts match")
print(
"Native model schemas differ; the existing public schema adapter is preserved."
)リポジトリでの移行手順
- 移行概要の作成。 対象モジュール、直接の呼び出し元、担当者、承認済みのバージョン、契約テスト、対象外のコード、ロールバック手順を明記します。今回の移行に、対象範囲を超える依存パッケージのアップグレードや公開 API の動作変更が必要なら、作業を中断します。
- ベースラインの確立。 Codex に
AGENTS.md、移行概要、公式の移行ガイド、ロックファイル、呼び出し元、テストを確認させます。開始時点のコミットとインストール済みのバージョンを記録します。ベースラインで失敗するチェックがあれば、移行前後の動作が一致すると結論付ける前に、失敗の理由を説明するか、原因を修正します。 - 互換性確保のための準備。 共有の依存パッケージを通じてモジュールが他のコードと結び付いている場合は、準備段階の変更を先に単独でレビューします。元のベースラインと準備後のベースラインの両方で、同じフィクスチャを使ったテストが合格することを確認します。
- 対象を絞った移行。 このページの開始用プロンプトを使用します。変更はモジュールとそれに直接対応するアダプターに限定し、無関係なモジュールはそのまま残します。残した互換性維持用のコードを明記します。
- 同じテストの再実行。 期待値を変更せず、HTTP とスキーマのフィクスチャを使ってベースラインと最終実装を比較します。影響範囲の統合テスト、lint チェック、型チェックを実行し、最終的なソースと依存関係の状態に対応付けて出力を保存します。
- 引き継ぎ内容のレビュー。 差分、実行結果の記録、未解決の差異、残っている互換ブリッジ、ロールバック手順を確認します。リポジトリのツールと権限で作成できる場合に限り、ドラフト PR を作成します。マージ、デプロイ、次の範囲の移行はレビュアーに委ねます。
検証結果の記録
検証基準ごとに、次の形式で記録を残します。
Criterion: [behavior and test]
Environment: [setup reference; actual runtime and dependency versions]
Before: [commit/diff ID; command; exit code; observed result; evidence path]
After: [final commit/diff ID; command; exit code; observed result; evidence path]
Status: [pass / fail / blocked / not run]
Limits: [untested behavior, missing prerequisite, or required owner decision]
インポートや lint チェックが成功しても、あるいはスナップショットを再生成しても、それだけでは API の動作が一致することを確認できません。意図した差異は別途記録し、最終版のソースに変更が入ったらチェックを再実行します。サンプルを自分のアプリケーションに合わせて使う際は、実際の呼び出し元とサービスも契約テストに含めてください。