結果オブジェクトを in-place で書き換えると事故る
2026.08.25 09:00
2026.08.30 00:35
AIの構造化出力をスキーマで検証し、無事に通った。そこまで確認できると、あとは表示や保存に合わせて少しくらい加工しても大丈夫だと思ってしまいます。
自分も表示用のラベルを足し、不要に見えたフィールドをその場で削除しました。その処理自体は最後まで動いたのですが、あとになってセッションを再開しようとしたところ、検証もdigest照合も通らなくなりました。原因は、検証済みの結果オブジェクトを直接書き換えていたことでした。
for (const item of result.items) {
item.label = buildLabel(item);
delete item.origin_file;
}
await save(result);
origin_file がスキーマの必須項目で、内容のdigestも照合している場合、再開時の再検証とハッシュ照合が同時に失敗します。問題が表面化するのは変更時ではなく再開時なので、原因も追いにくくなります。
原型と保存用データを分ける
検証済みの原型はそのままセッションへ保存し、派生値はコピーへ追加します。
await saveSession({ result, digest });
const forSave = result.items.map(item => ({
...item,
label: buildLabel(item),
}));
await save(forSave);
不要なフィールドを取り除く場合も、原型から delete せず分割代入で保存用オブジェクトを作ります。
const forSave = result.items.map(({ origin_file, ...rest }) => ({
...rest,
label: buildLabel(rest),
}));
照合してから保存する
NG: digest保存 → セッション保存 → origin照合
OK: origin照合 → digest保存 → セッション保存
不整合な状態を保存したあとで検出しても復旧が難しくなります。originのスナップショット照合を先に行います。
直してみれば、必要だったルールは単純でした。検証済みのオブジェクトは原型として残し、表示用・保存用の加工はコピーに対して行う。それだけで、再検証とdigest照合の前提を壊さずに済みます。
その場では便利な1行の代入も、壊れるのが再開時だと原因を追うのに時間がかかります。境界でコピーを作るか、必要なら Object.freeze() する。検証後のデータを読み取り専用として扱う習慣が、あとから効いてきました。
今回は以上です。