PHP7.1 json_encode() Float Issue
Asked Answered
P

14

150

This isn't a question as it is more of a be aware. I updated an application that uses json_encode() to PHP7.1.1 and I was seeing an issue with floats being changed to sometimes extend out 17 digits. According to documentation, PHP 7.1.x started to use serialize_precision instead of precision when encoding double values. I'm guessing this caused an example value of

472.185

to become

472.18500000000006

after that value went through json_encode(). Since my discovery, I have reverted back to PHP 7.0.16 and I no longer have the issue with json_encode(). I also tried to update to PHP 7.1.2 before reverting back to PHP 7.0.16.

The reasoning behind this question does stem from PHP - Floating Number Precision, however the end all reason for this is because of the change from precision to serialize_precision usage in json_encode().

If anyone does know of a solution to this problem, I'd be more than happy to listen in on the reasoning/fix.

Excerpt from multidimensional array (before):

[staticYaxisInfo] => Array
                    (
                        [17] => stdClass Object
                            (
                                [variable_id] => 17
                                [static] => 1
                                [min] => 0
                                [max] => 472.185
                                [locked_static] => 1
                            )

                    )

and after going through json_encode()...

"staticYaxisInfo":
            {
                "17":
                {
                    "variable_id": "17",
                    "static": "1",
                    "min": 0,
                    "max": 472.18500000000006,
                    "locked_static": "1"
                }
            },
Phalangeal answered 23/3, 2017 at 16:11 Comment(11)
ini_set('serialize_precision', 14); ini_set('precision', 14); would probably make it serialize like it used to, however if you really rely on a specific precision on your floats you're doing something wrong.Overweight
It was defaulted as 17, but I changed it to -1 to have the "enhanced algorithm" take over. No dice.Phalangeal
"If anyone does know of a solution to this problem" -- what problem? I can't see any problem here. If you decode the JSON using PHP you get back the value you encoded. And if you decode it using a different language most probably you get the same value. Either way, if you print the value with 12 digits you get back the original ("correct") value. Do you need more than 12 decimal digits precision for the floats used by your application?Savoie
@Savoie 472.185 != 472.18500000000006. There is a clear before and after difference.This is part of an AJAX request to a browser and the value needs to stay in it's original state.Phalangeal
1/3 != 0.333333333333333 either but we usually think them as being equal and we represent 1/3 as 0.33 or similar. The floating point numbers are approximations of the real numbers they represent. This happens both in real-life and in computers; only the set of numbers that cannot be represented exactly differs. Back to my original question: do you really need 14 decimal digits precision in your numbers? I guess not. Just format the number as string in PHP (using number_format() f.e.) and pass the string to json_encode().Savoie
I am trying to avoid using a string conversion as the end product is Highcharts and it will not accept strings. I think I'd consider it to be very inefficient and sloppy if you take a float value, cast it as a string, send it away, and then have javascript interpret the string back to a float with parseFloat(). Don't you?Phalangeal
@Savoie I note that you're PHP json_decode() does bring back the original float value. However, when javascript turns the JSON string back to an object, it does not convert the value back to 472.185 like you had potentially insinuated...hence the problem. I will stick with what I have going.Phalangeal
@apokryfos,if I set ini_set('serialize_precision', 100); ini_set('precision', 100); in PHP 5.X,would be better?Light
There are two places where you had 472.185, in your sourcecode and in the JSON-serialized form. In memory, you had a binary representation that approximates this decimal representation. The serialization has been improved to better approximate what you had in memory. In my eyes, the expectations of the JS code are flawed, expecting a certain kind of representation is not reliable.Nightclub
This serialisation may create a problems with APIs. Sample: PayPal REST SDK sends 'jsoned' array to PayPal server and e.g. 461.61 becomes 461.60999999999996, which is not accepted by PayPal server. There are other APIs with same problem, so this is an issue and not a trivial one @axiac. Fortunately in this case one can use strval, but still, it is an issue.Cb
From various answers here and the documentation, I found that ini_set('serialize_precision', -1); works for me.Hug
K
133

This drove me nuts for a bit until I finally found this bug which points you to this RFC which says

Currently json_encode() uses EG(precision) which is set to 14. That means that 14 digits at most are used for displaying (printing) the number. IEEE 754 double supports higher precision and serialize()/var_export() uses PG(serialize_precision) which set to 17 be default to be more precise. Since json_encode() uses EG(precision), json_encode() removes lower digits of fraction parts and destroys original value even if PHP's float could hold more precise float value.

And (emphasis mine)

This RFC proposes to introduce a new setting EG(precision)=-1 and PG(serialize_precision)=-1 that uses zend_dtoa()'s mode 0 which uses better algorigthm for rounding float numbers (-1 is used to indicate 0 mode).

In short, there's a new way to make PHP 7.1 json_encode use the new and improved precision engine. In php.ini you need to change serialize_precision to

serialize_precision = -1

You can verify it works with this command line

php -r '$price = ["price" => round("45.99", 2)]; echo json_encode($price);'

You should get

{"price":45.99}
Kellie answered 27/3, 2017 at 20:51 Comment(4)
G(precision)=-1 and PG(serialize_precision)=-1 can also be used in PHP 5.4Light
Be careful with serialize_precision = -1. With -1, this code echo json_encode([528.56 * 100]); prints [52855.99999999999]Tillery
@Tillery That sounds more like a general floating point error, though. Here's a demo, where you can see it clearly isn't just a json_encode problemKellie
I thought this was just PHP being daft, but it turns out WHM (the nasty configuration manager our server uses) decided to set serialize_precision to 100 by default, so every time we'd call json_encode we'd get 100-digit-long values. Somebody must have gone "precision? Yeah, we'll have lots of that!" without having any idea what they were doing. Commenting that out and using the default (which is -1 now) fixed it.Gowon
S
61

As a plugin developer I don't have general access to the php.ini settings of a server. So, based on Machavity's answer I wrote this small piece of code that you can use in your PHP script. Simply put it on top of the script and json_encode will keep working as usual.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'serialize_precision', -1 );
}

In some cases it is necessary to set one more variable. I am adding this as a second solution because I am not sure if the second solution works fine in all cases where the first solution has proven to work.

if (version_compare(phpversion(), '7.1', '>=')) {
    ini_set( 'precision', 17 );
    ini_set( 'serialize_precision', -1 );
}
Sigismundo answered 24/9, 2017 at 13:6 Comment(4)
Take care with that, as your plugin might change unexpected settings for the rest of the developer application. But, IMO, I'm not sure how destructive this option could get... lolGewgaw
Be aware that change precision value (second example) could have a bigger impact in other mathematical operations you have there. php.net/manual/en/ini.core.php#ini.precisionRoller
@RicardoMartins: As per documentation the default precision is 14. Above fix increases this to 17. So it should be even more precise. Do you agree?Sigismundo
@alev what I was saying is that by changing just the serialize_precision is enough and don't compromise other PHP behaviors your application may experienceRoller
D
25

I solved this by setting both precision and serialize_precision to the same value (10):

ini_set('precision', 10);
ini_set('serialize_precision', 10);

You can also set this in your php.ini

Denote answered 27/8, 2019 at 8:16 Comment(1)
this was the only one that worked for me.. setting Both values. Or setting serialize_precision to 14 as that was the default for precision, but safer to set both values to the same thing.Levi
H
6

I was encoding monetary values and had things like 330.46 encoding to 330.4600000000000363797880709171295166015625. If you don't wish to, or can't, change the PHP settings and you know the structure of the data in advance there is a very simple solution that worked for me. Simply cast it to a string (both the following do the same thing):

$data['discount'] = (string) $data['discount'];
$data['discount'] = '' . $data['discount'];

For my use case this was a quick and effective solution. Just note that this means when you decode it back from JSON it will be a string since it'll be wrapped in double quotes.

Hexyl answered 20/6, 2018 at 9:8 Comment(1)
If having it as a string in JSON is an issue, just cast it back to float, e.g. (float)(string) $data['discount'];Xerophilous
U
5

On php 7.2.32 the solution was to set in php.ini:

precision=10
serialize_precision=10
Unifoliolate answered 7/4, 2021 at 8:32 Comment(0)
L
4

I had the same problem but only serialize_precision = -1 did not solve the problem. I had to do one more step, to update the value of precision from 14 to 17 (as it was set on my PHP7.0 ini file). Apparently, changing the value of that number changes the value of the computed float.

Lenrow answered 22/12, 2017 at 8:49 Comment(0)
B
2
$val1 = 5.5;
$val2 = (1.055 - 1) * 100;
$val3 = (float)(string) ((1.055 - 1) * 100);
var_dump(json_encode(['val1' => $val1, 'val2' => $val2, 'val3' => $val3]));
{
  "val1": 5.5,
  "val2": 5.499999999999994,
  "val3": 5.5
}
Beast answered 20/4, 2020 at 13:46 Comment(0)
U
1

As for me the problem was when JSON_NUMERIC_CHECK as second argument of json_encode () was passed, which casting all (not only integer) numbers type to int.

Umber answered 16/8, 2019 at 12:54 Comment(0)
N
1

Store it as a string with the exact precision that you need by using number_format, then json_encode it using the JSON_NUMERIC_CHECK option:

$foo = array('max' => number_format(472.185, 3, '.', ''));
print_r(json_encode($foo, JSON_NUMERIC_CHECK));

You get:

{"max": 472.185}

Note that this will get ALL numeric strings in your source object to be encoded as numbers in the resulting JSON.

Nimwegen answered 28/10, 2019 at 17:39 Comment(1)
I have tested this in PHP 7.3 and it does not work (output still has too high precision). Apparently the JSON_NUMERIC_CHECK flag is broken since PHP 7.1 - php.net/manual/de/json.constants.php#123167Patience
T
0

It seems like the problem occurs when serialize and serialize_precision are set to different values. In my case 14 and 17 respectively. Setting them both to 14 resolved the issue, as did setting serialize_precision to -1.

The default value of serialize_precision was changed to -1 as of PHP 7.1.0 which means "an enhanced algorithm for rounding such numbers will be used". But if you are still experiencing this issue, it may be because you have a PHP config file in place from a prior version. (Maybe you kept your config file when you upgraded?)

Another thing to consider is if it makes sense to be using float values at all in your case. It may or may not make sense to use string values containing your numbers to ensure the proper number of decimal places is always retained in your JSON.

Torr answered 23/10, 2020 at 17:24 Comment(0)
N
-1

Same issue. On PHP 7.4 I tried different solutions but only this combination worked for me:

precision = 14
serialize_precision = 14
Norrie answered 6/5, 2022 at 9:21 Comment(0)
B
-1

I did not want to mess with serialize_precision so I wrote my own simple encoder:

function JsonEncode($data) {
    $tr = [
        '"' => '\\"',
        '\\' => '\\\\',
        '/' => '\\/',
        "\010" => '\\b',
        "\f" => '\\f',
        "\n" => '\\n',
        "\r" => '\\r',
        "\t" => '\\t'
    ];
    if (is_object($data)) {
        $list = [];
        foreach (get_object_vars($data) as $k => $v) {
            $list[] = sprintf('"%s":%s', strtr($k, $tr), JsonEncode($v));
        }
        $json = sprintf('{%s}', implode(',', $list));
    } elseif (is_array($data)) {
        $list = [];
        foreach ($data as $v) {
            $list[] = JsonEncode($v);
        }
        $json = sprintf('[%s]', implode(',', $list));
    } elseif (is_string($data)) {
        $json = sprintf('"%s"', strtr($data, $tr));
    } elseif (is_bool($data)) {
        $json = $data ? "true" : "false";
    } elseif (is_null($data)) {
        $json = 'null';
    } else {
        $json = (string)$data;
    }
    return $json;
}
Ballou answered 28/5, 2023 at 9:13 Comment(0)
I
-1

This does the trick. When converting from string to double or just rounding numbers, you won't get those long decimals with this php.ini/in-line setting:

ini_set('serialize_precision', 14);

Tested on PHP 8.3

Internationalist answered 25/3, 2024 at 22:49 Comment(1)
It was mentioned in 2 answers alreadyFlossieflossy
E
-2

You could change the [max] => 472.185 from a float into a string ([max] => '472.185') before the json_encode(). As json is a string anyway, converting your float values to strings before json_encode() will maintain the value you desire.

Electrolytic answered 15/5, 2019 at 15:51 Comment(2)
This is technically true to a certain extent, but very inefficient. If a Int/Float in a JSON string is not quoted, then Javascript can see it as an actual Int/Float. Performing your rendition forces you to cast every single value back to a Int/Float once on the browser side. I was often dealing with 10000+ values when working on this project per request. A lot of bloat processing would have ended up happening.Phalangeal
If you are using JSON to send data somewhere, and a number is expected but you send a string, that is not guaranteed to work. In situations where the developer of the sending application doesn't have control over the receiving application, this isn't a solution.Chinatown

© 2022 - 2025 — McMap. All rights reserved.