PHPにおけるJSON浮動小数点数の精度制御と文字列表現の実装

PHPのjson_encode()は、浮動小数点値をJSON文字列に変換する際、デフォルトで末尾のゼロ(例:1.001.01)や不要な小数部を削除します。これは内部的な表示形式によるものであり、JSON仕様上は問題ありませんが、外部システム(特にJavaやTypeScriptなど型厳密な環境)との連携時に署名検証失敗や比較不一致を引き起こすことがあります。

標準オプションによる精度保持は不可能

json_encode()の第2引数であるフラグには、JSON_PRESERVE_ZERO_FRACTIONがありますが、これは「小数点以下が存在し、かつその先頭が0の場合に.0を付与」する挙動に過ぎず、任意の小数桁数(例:2桁固定)を保証しません。

以下のコードで確認できます:

$data = ['price' => 99.00, 'rate' => 12.50, 'tax' => 8.333];
echo json_encode($data, JSON_PRESERVE_ZERO_FRACTION);
// 出力: {"price":99.0,"rate":12.5,"tax":8.333}

同様にJSON_NUMERIC_CHECKは文字列としての数字を数値へ変換するだけで、表示精度には一切関与しません。またserialize_precision設定も、内部的な出力精度を調整するものであり、JSONエンコーディング時の整形には影響を与えません。

確実な解決策:事前文字列化+正規表現後処理

要件が「すべての数値フィールドを小数点以下2桁の文字列として出力」である場合、最も信頼性の高いアプローチは、JSON変換前に数値を明示的にフォーマットし、その後JSON構造内で数値リテラルとして扱われないよう文字列として保持することです。

以下は汎用性のある実装例です:

function formatFloatsAsStrings(array $input, int $decimals = 2): array
{
    $formatter = fn($val) => is_float($val) || (is_int($val) && strpos((string)$val, '.') !== false)
        ? sprintf('%.' . $decimals . 'f', $val)
        : $val;

    return array_map(function ($value) use ($formatter) {
        if (is_array($value)) {
            return formatFloatsAsStrings($value, $decimals);
        }
        if (is_object($value) && method_exists($value, '__toArray')) {
            return formatFloatsAsStrings($value->__toArray(), $decimals);
        }
        return $formatter($value);
    }, $input);
}

// 使用例
$payload = [
    'amount' => 199.9,
    'discount' => 15.00,
    'items' => [['unit_price' => 24.995, 'qty' => 2]]
];

$json = json_encode(formatFloatsAsStrings($payload));
// 出力: {"amount":"199.90","discount":"15.00","items":[{"unit_price":"25.00","qty":2}]}

この手法では、数値が文字列としてエンコードされるため、JSON側での解釈誤差や精度喪失が発生しません。必要に応じて再帰処理やオブジェクト対応を追加可能です。

代替案:JSONエンコード後の文字列置換(限定的推奨)

構造が単純で、数値がネストしていない場合、JSON生成後に正規表現で置換する方法もありますが、安全性と保守性の観点から推奨されません:

$rawJson = json_encode($data);
// 単純な数値リテラル(例:"123.45")を検出し、ダブルクォートを除去して再フォーマット
$fixedJson = preg_replace_callback(
    '/"(\d+\.\d+)"/',
    function ($matches) {
        return sprintf('%.2f', (float)$matches[1]);
    },
    $rawJson
);

ただし、この方法はJSON内の文字列値(例:"version": "2.0.0")を誤って変換するリスクがあり、実運用では避けるべきです。

補足:浮動小数点の本質的理解

PHPのfloat型はIEEE 754準拠の64ビット倍精度であり、「1.0」と「1.00」はメモリ上同一のバイナリ値です。したがって、var_dump(1.00)float(1)と表示されるのは当然の挙動です。小数点以下の桁数はあくまで表示情報であり、値の意味論とは無関係です。

したがって、精度を保つ必要がある場合は、常に文字列として扱うか、bcmul()/bcadd()などのBCMath関数を用いた正確な10進演算を選択すべきです。

タグ: PHP JSON float precision Serialization

7月19日 01:05 投稿