45395 - シコウサクゴ -

外部APIの『未成功=0』が量計算で0埋めされる罠:`if x` でなく `> 0` でガードする

2026-06-14
AI駆動開発
AI駆動開発
Claude Code
フォールバック
境界値
バリデーション
Last updated:2026-06-14
14 Minutes
2712 Words

外部サービスへの書き込み操作(以下「外部 API コール」)が未成功・未確定だったときに、レコードが一切記録されないというバグに遭遇しました。エラーで派手に落ちるのではなく、100%静かにスキップされるという、いやらしい表面化のしかたでした。

原因を追っていくと、問題の核心は「外部 API が失敗を 0 という正常値で返していた」ことにありました。null ではなく 0。この一点が、量計算の下流に静かに 0 を流し込もうとしていたのです。

本記事は、このバグの構造と修正、そして「外部値の 0null を区別せよ」「量系は > 0 でガードせよ」という普遍的な学びの記録です。

事の発端:未成功のはずが「0件・0.0」で返ってくる

バグの構造はこうでした。外部 API コールが未成功・未確定だったとき、外部 API のレスポンスは次のような値を返していました。

1
# 外部APIの未成功時のレスポンス(イメージ)
2
response.result_count = 0 # 件数 = 0
3
response.result_value = 0.0 # 値 = 0.0

コードはこの 0 / 0.0有効な値として受け取り、永続化テーブルに「数量 0」のレコードを書こうとしていました。失敗を表す null なら弾けたかもしれませんが、返ってきたのは型としては完全に正常な 0 です。truthy 判定や is not None 判定のすり抜けを起こすのに、これ以上ない値です。

つまり、**「失敗したのに、失敗とは見なされない値」**が下流に流れていた、というのが発端でした。

なぜ問題か:0 は「失敗」と「正常な値」の両方になりうる

null(None)であれば、「値が無い=失敗・未確定」とほぼ一意に解釈できます。ところが 0 は事情が違います。

受け取り側の判定null の扱い0 の扱い量計算への影響
if value:(truthy)弾けるすり抜ける(0 は falsy だが…後述)文脈次第で危険
if value is not None:弾けるすり抜ける0 がそのまま下流へ
if value > 0:(要 None ガード)弾ける安全

0 は「外部 API が失敗を表すために返した値」かもしれないし、「本当に件数が 0 だった正常な結果」かもしれません。同じ 0 が、文脈によって失敗にも正常にもなりうる——ここが落とし穴です。

そして、数量・金額相当の数値・件数といった「正であるべき量」にとって、0 の混入は単なる無効値ではありません。掛け算・割り算・集計の途中に紛れ込むと、下流全体を 0 埋めする力を持ちます。if value is not None で受けていると、この 0 が検査をすり抜けて、量計算の根元に静かに座ってしまうのです。

救ったのは下流の CHECK 制約だった

幸い、今回のバグは「静かに 0 が保存される」までは至りませんでした。理由は、永続化テーブルのレコード型(dataclass)に、最後の砦があったからです。

1
@dataclass
2
class Record:
3
quantity: int
4
# ...
5
def __post_init__(self):
6
if self.quantity <= 0:
7
raise ValueError("quantity は正でなければなりません")

__post_init__quantity > 0(数量は正)という CHECK 制約が入っていました。そのため、数量 0 のレコードを書こうとすると ValueError が送出され、記録に失敗していたのです。

結果として、バグの実害は「未成功時にレコードが記録されず 100% スキップされる」という形で表面化しました。沈黙のうちに 0 埋めデータが保存されるのではなく、明示的に弾かれていたわけです。

これは、この CHECK 制約が暗黙の検出機構として働いていたことを意味します。本来あってはならない不正値(数量 0)が、保存される前に確実に止められていました。「沈黙の失敗」を「可視化された失敗」に変換していた点で、この制約は非常に良い仕事をしていました。

修正:is not None ではなく > 0 の境界ゲートを入れる

修正の方向は明快です。フォールバック値を採用する条件を、「None でなければ採用」から「正の値のときだけ採用」に変えることでした。

1
# Before: None でなければ採用 → 0 がすり抜ける
2
if fallback is not None:
3
quantity = fallback
4
5
# After: 正の値のときだけ採用 → 0 は採用しない
6
if fallback is not None and int(fallback) > 0:
7
quantity = fallback
8
else:
9
quantity = units # 別経路:事前に算出済みの正の数量へフォールバック

fallback0 のときは採用せず、事前に算出済みの正の数量(units)にフォールバックする > 0 境界ゲートを入れました。

値(金額相当の数値)についても同様です。

1
# After: 値も > 0 のときだけ採用
2
if result_value is not None and float(result_value) > 0:
3
amount = result_value
4
else:
5
amount = signal_value # 別の信頼できる値(シグナル側の値)を使う

0 のときは、別の信頼できる値(シグナル側の値)を使うようにしました。本番経路では、この別経路のフォールバック値(units)が呼び出し元で必ず正の数量として渡されることをコード上で確認しています(quantity > 0 が保証されます)。これで、未成功時でも下流に正の量が渡り、レコードが正しく記録されるようになりました。

なお、修正コードには「なぜ > 0 判定が必要か」を将来の読み手向けに説明するコメントを添えました。0 を弾く意図がコメントとして残っていないと、後から「この > 0 は冗長では?」と削られて再混入する恐れがあります。意図を言語化して残すことで、同じバグの再混入を抑止しています。

学び:量系は truthy でなく境界で受ける、そして下流に制約を置く

このケースを普遍化すると、2つの設計原則になります。

1つめ:外部値の 0null は区別する。

外部 API は「失敗・未確定」を、null ではなく 0 という正常範囲の値で返すことがあります。これを if value:(truthy 判定)や if value is not None(None だけ弾く)で受けると、0 がすり抜けて下流の量計算・金額計算・カウントに 0 埋めとして混入します。数量・金額・件数のような「正であるべき量」は、truthy でも is not None でもなく、明示的な > 0 の境界検証で受けるべきです。

2つめ:下流に > 0 の制約を置き、最後の砦にする。

入力側のガードは漏れることがあります。だからこそ、下流に > 0 の CHECK 制約(DB やデータ型レベル)を置いておくと、それが最後の砦=暗黙の検出機構になります。今回の dataclass の制約のように、不正な 0 が静かに保存されるのを防ぎ、沈黙の失敗を可視化してくれます。「入口でガード」と「出口でガード」の二重化が効きます。

防御の層役割今回の具体
入口(境界ゲート)不正な 0 を採用しないif fallback is not None and int(fallback) > 0
入口(代替経路)0 のとき別の正値へunits / シグナル側の値へフォールバック
出口(CHECK 制約)万一すり抜けても保存させないdataclass の quantity > 0
文書(コメント)意図を残し再混入を防ぐ「なぜ > 0 か」の説明

運用Tips:AI にフォールバックを書かせるときの指示

この学びは、AI にフォールバック処理を書かせるときの指示にそのまま落とせます。放っておくと、AI は素直に if value is not None: と書きがちだからです。

依頼テンプレートはこうしています。

1
## フォールバック処理の実装依頼
2
3
外部APIのレスポンスからフォールバック値を採用する処理を書く。
4
5
必須要件:
6
1. 外部値の「0」と「null」は区別すること
7
(外部APIは失敗を null ではなく 0 で返すことがある)
8
2. 数量・金額・件数など「正であるべき量」は
9
`if value`(truthy)や `is not None` ではなく、
10
明示的な `> 0` の境界検証で受けること
11
3. 0 のときは別の信頼できる経路(事前算出済みの正値など)へ
12
フォールバックすること
13
4. 下流のデータ型/DBに `> 0` の制約を置き、
14
万一の不正な0が静かに保存されないようにすること
15
5. 「なぜ > 0 判定が必要か」をコメントで残し、
1 collapsed line
16
後から冗長と判断されて削られないようにすること

特に 2 と 5 を明示しておくのが肝です。> 0 ガードは一見すると冗長に見えるので、意図を言語化しておかないと簡単に剥がれます

まとめ:0 は「無効」とは限らない

null は素直に「値が無い」を意味してくれますが、0 はそう単純ではありません。外部 API にとって 0 は、失敗を表す値にも、正常な結果にもなりえます。

今回の学びをまとめます。

  1. 外部 API は失敗を null ではなく 0 で返すことがある
  2. if value(truthy)や is not None では 0 がすり抜ける
  3. 「正であるべき量」は 明示的な > 0 で受け、0 のときは別の正値へフォールバックする
  4. 下流に > 0 の CHECK 制約を置き、最後の砦=暗黙の検出機構にする
  5. 「なぜ > 0 か」をコメントで残し、再混入を防ぐ

AI にフォールバックを書かせるときは、「外部値の 0null は区別し、量系は > 0 でガードせよ」「下流に制約を置いて沈黙の不正値を弾け」と指示します。たった一段の境界検証ですが、これがあるかどうかで、静かに 0 で埋まったデータが本番に流れるか否かが決まります。

Article title:外部APIの『未成功=0』が量計算で0埋めされる罠:`if x` でなく `> 0` でガードする
Article author:45395
Release time:2026-06-14

記事へのご質問・ご感想をお聞かせください

フィードバックを送る