レガシーシステムとモダンAPIの間での『データ型の暗黙の変換』がなぜ本番で問題になるのか
開発環境では「うまくいく」、本番では「ずれる」
基幹系のシステムと新しいモバイルアプリを連携させる案件で、こんな場面に遭遇することがあります。
古いシステムは COBOL や Java で書かれた 20 年前の資産で、数値データは固定長の文字列として保存されている。一方、新しい API は JSON で数値を返す。開発環境では「きちんと変換されている」と見えるのに、本番環境で「金額がずれている」「在庫数が異なる」という報告が上がってくる。
これは単なる「テスト漏れ」ではなく、暗黙の型変換が環境やタイミングによって異なる挙動をするためです。本記事では、この問題がなぜ起きるのか、そして現場でどう対処するのかを掘り下げます。
なぜ「暗黙の変換」は本番環境で裏切るのか
開発環境と本番環境の違い
開発マシンでテストするとき、データは往々にして「理想的な状態」です。数値は正確に格納され、文字列は期待した長さです。しかし本番環境では:
- レガシーシステムからのデータが正規化されていない ―スペースパディング、ゼロパディング、符号の表記揺れが混在している
- API層での型推論が環境依存 ― JavaScript の JSON パーサーは
"123"を数値に変換するが、"123.45"や"123.45 "の扱いは実装次第 - データベースドライバの挙動が異なる ― 古い JDBC ドライバと新しい Node.js ドライバでは、NULL や空文字列の解釈が異なることがある
具体的には、以下のようなケースが起きます:
レガシー側: "00123 " (7文字、右詰め)
↓
API側: JSON.parse() で "00123 " として返す
↓
フロント側: Number("00123 ") → 123 (成功)
↓
計算処理: 123 + 456 = 579 (正しい)
しかし、別のタイミングで:
レガシー側: "00123.50" (小数を含む)
↓
API側: parseFloat("00123.50") → 123.5
↓
フロント側: 123.5 * 100 = 12349.999999... (浮動小数点誤差)
↓
DB保存: 12349 (丸め処理で切り詰め) ← 問題発生
「動いているように見える」理由
開発環境では、テストデータが「キレイな」データに限定されるため、変換がうまくいきます。しかし本番では:
- 数年分の蓄積データが混在している
- 手作業で入力されたデータに揺れがある
- システム間の連携で予期しない値が流入している
という現実があります。
現場で起きやすい「型変換の失敗パターン」
パターン 1: 文字列と数値の曖昧な境界
// API から返ってくるデータ
const response = {
amount: "1000", // 文字列?数値?
quantity: "00050", // ゼロパディング
price: "99.99" // 小数
};
// 単純な変換
const total = Number(response.amount) * Number(response.quantity);
// 1000 * 50 = 50000 (正しい)
// しかし、データに空白が含まれていたら?
const amount2 = "1000 "; // 末尾に空白
Number(amount2); // → 1000 (JavaScript は許容)
// しかし他の言語では?
Integer.parseInt("1000 "); // Java → NumberFormatException
パターン 2: NULL と空文字列の扱い
レガシーシステムでは NULL と空文字列が区別されないことが多いです。
// レガシー側の戻り値
{
optional_field: "" // NULL なのか、本当に空文字列なのか?
}
// API側での処理
if (data.optional_field) {
// 空文字列は falsy なので、ここに入らない
}
// しかし DB 側では
INSERT INTO table (field) VALUES (""); // 空文字列として保存
// vs
INSERT INTO table (field) VALUES (NULL); // NULL として保存
// → 後続の集計クエリで結果が異なる
パターン 3: 浮動小数点誤差の累積
金額計算では特に危険です。
// API から返ってくる価格
const price = 0.1 + 0.2; // JavaScript では 0.30000000000000004
const quantity = 100;
const total = price * quantity; // 30.000000000000004
// DB に保存するとき
Math.round(total * 100) / 100; // 30.00 に丸まる
// しかし、複数の計算が重なると、丸めの誤差が蓄積する
設計と実装での対策
1. 型変換を「明示的」にする
// ❌ 暗黙の変換に頼る
const amount = response.amount * 1;
// ✅ 明示的に変換し、失敗時は例外を発生させる
function parseAmount(value) {
const trimmed = String(value).trim();
if (!/^\d+(\.\d{1,2})?$/.test(trimmed)) {
throw new Error(`Invalid amount format: ${value}`);
}
return parseFloat(trimmed);
}
const amount = parseAmount(response.amount);
2. レガシー側のデータ形式を文書化する
API の仕様書に「金額は COBOL の NUMERIC(9,2) として格納されており、返却時は文字列で右詰めされます」と明記する。これにより、受け取り側は期待値を把握できます。
3. 本番データの「実サンプル」でテストする
開発環境のテストデータではなく、本番環境から抽出した実データで変換テストを実施します。
// 本番から抽出した実データセット
const realData = [
{ amount: "00001000", expected: 1000 },
{ amount: "1000 ", expected: 1000 }, // スペースパディング
{ amount: "0", expected: 0 },
{ amount: "999999999", expected: 999999999 }
];
realData.forEach(({ amount, expected }) => {
const result = parseAmount(amount);
console.assert(result === expected, `Failed for ${amount}`);
});
4. API レイヤーでの検証を厳格にする
// Express.js の例
app.get('/api/items/:id', (req, res) => {
try {
const data = fetchFromLegacy(req.params.id);
// 各フィールドを検証して返す
const validated = {
amount: validateAmount(data.AMOUNT),
quantity: validateQuantity(data.QUANTITY),
date: validateDate(data.DATE)
};
res.json(validated);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
運用フェーズでの監視ポイント
本番リリース後、以下を監視します:
- 金額や数量の集計値の乖離 ― レガシーシステムの集計結果と新 API の集計結果を定期的に比較
- エラーログの監視 ― 型変換エラーが発生していないか
- データの整合性チェック ― 月次で本番データを抽出し、変換ロジックが正しく動作しているか確認
特に、金融や在庫に関わるシステムでは、この監視を甘く見ると後々大きな問題になります。
中小規模の開発組織が現実的に取れるアクション
- レガシーシステムのデータ仕様書を整備する ― 既存システムの担当者に聞き取り、データ形式を文書化
- API の戻り値スキーマを厳密に定義する ― JSON Schema などで型と範囲を明記
- 本番データでの回帰テストを自動化する ― 月 1 回、本番から抽出したサンプルで変換テストを実行
- エラーハンドリングを設計段階で組み込む ― 予期しない値が来たときの処理を明確に
レガシーシステムとの連携は、「古いから仕方ない」で済ませるのではなく、その仕様を理解し、明示的な変換処理を施すことが、結果的に運用コストを下げます。