json - 如何使用 serde_json 序列化包含 f32 的结构?

标签 json rust serde reqwest serde-json

Rust 相对较新。我正在尝试进行需要序列化 ​​JSON 正文的 API 调用。

JSON 正文包含一个 order_amount 键,其值只能采用 INR 格式的值 100.36,即 100 卢比和 36 派萨。更多示例 10.48、3.20、1.09。

我面临的问题是,在使用来自 serde_json 的 json!() 序列化后,浮点值变成类似于 100.359765464332。

API 随后失败,因为它预计 order_amount 只有两位小数。

这是我的代码:

进口

use lambda_runtime::{handler_fn, Context, Error};
use reqwest::header::ACCEPT;
use reqwest::{Response, StatusCode};
use serde_json::json;
use std::env;

#[macro_use]
extern crate serde_derive;

我正在序列化的结构

#[derive(Serialize, Deserialize, Clone, Debug)]
struct OrderCreationEvent {
    order_amount: f32,
    customer_details: ...,
    order_meta: ...,
}

例如。这里的order_amount值为15.38

async fn so_my_function(
    e: OrderCreationEvent,
    _c: Context,
) -> std::result::Result<CustomOutput, Error> {
let resp: Response = client
        .post(url)
        .json::<serde_json::Value>(&json!(e))
        .send()
        .await?;

在 json!() 之后,金额被序列化为 15.379345234542。我需要 15.38

我读了一些关于为 f32 编写自定义序列化程序的文章,它可以截断为 2 位小数,但我对 Rust 的熟练程度有限。

所以,我找到了这段代码并一直在修改它,但没有成功:

fn order_amount_serializer<S>(x: &f32, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_f32(*x)
    // Ok(f32::trunc(x * 100.0) / 100.0)
}

无论自定义序列化器是否是正确的方法或解决问题的方法,我仍然想学习如何编写一个,所以也请随意启发我。 干杯! :)

最佳答案

TL;DR 这是一个浮点问题,来自 serde_jsonf32 扩展为 f64。您可以使用像 println!("{}", 77.63_f32 as f64) 这样简单的代码重现它。要修复它,您需要转换为 f64,然后舍入,并将其序列化为 f64:

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)

详细解释

代码中的问题出在与您认为不同的地方 - 它与浮点精度有关,而不是与 serde。当你写这样的东西时:

let lat = 77.63_f32;

...您指示编译器将分数 7763/100 转换为 f32。但是这个数字不能用 f32 精确表示,因为 f32(像所有二进制浮点类型一样)使用二进制分数,即分母是一定大小内的 2 的幂的有理数限制。鉴于这些限制,7763/100 近似为 10175119/2**17。1 如果您尝试打印该 f32 值,您将 get the expected 77.63输出是因为 println!() 知道它正在打印一个 f32,其中第 7 位之后的所有数字都是近似值的副作用并被丢弃。

serde_json 的工作方式不同 - 它通过 converting them to f64 序列化 f32 值因为这是 JSON 和 JavaScript 使用的精度。不幸的是,77.63_f32 的 10175119/2**17 近似值在没有最初存储 77.63 的上下文的情况下被扩大到 f64f64 只是存储近似值(它可以精确容纳,而不会进一步损失精度),当您打印结果 f64 时,您 get 77.62999725341797,这就是 10175119/2**17 十进制到 16 位精度的样子。

这就是为什么将自定义序列化实现为 s.serialize_f32(f32::trunc(*x * 100.0)/100.0) 没有效果 - 你舍入了 f32到两位小数(这在您的程序中是一个空操作,因为它一开始就是四舍五入的),然后您将它传递给 serialize_f32()serialize_f32() 继续将 f32 值扩大到 f64,这使得 f32 近似值的额外数字可见 -并且您回到了从 serde 生成的实现开始的地方。

正确的版本必须将f32转换为f64,然后去掉f64类型中多余的数字,然后传给serialize_f64() 用于打印:

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)

Playground

之所以可行,是因为:数字 77.63_f32 被转换为对应于 10175119/2**17 的 f64(即不是 77.63_f64,这将近似于 2 到 682840701314007/2**43)。然后,此数字在 f64 中四舍五入为两位数,并且该四舍五入产生了 f64 能够达到的最接近的近似值 77.63。 IE。现在我们得到了与在 Rust 源代码中使用 77.63_f64 得到的相同的 682840701314007/2**43 近似值。这是 serde 将使用的数字,serde_json 会将其格式化为 JSON 输出中的 77.63

旁注:上面的代码在问题的尝试之后使用了 trunc(),但也许是 round() as shown here会是更合适的选择。


1 您可以使用此 Python 单行代码获得此比率:

>>> numpy.float32("77.63").as_integer_ratio()
(10175119, 131072)

2 也使用 Python 获得:

>>> n = 10175119/131072
>>> rounded = round(n*100.0)/100.0
>>> rounded
77.63
>>> rounded.as_integer_ratio()
(682840701314007, 8796093022208)

关于json - 如何使用 serde_json 序列化包含 f32 的结构?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73871891/

相关文章:

rust - serde如何将字符串中的值转换为类型

java - 更新到 java 7 后,谷歌端点消息停止工作

json - 在 Javalin 框架中用 kotlinx.serialization 替换 Jackson

string - 如何连接字符串?

rust - 返回结构的类型不匹配(预期 <K, V>,找到 <&K, &V>)

rust - 如何使用 serde_json 和 Value 枚举处理可能缺失的字段?

java - 直接自引用导致循环父类(super class)问题 JSON

c# - Json.NET - 将 EmptyOrWhiteSpace 字符串属性转换为 null

rust - 在不声明变量的情况下重用值

rust - 如何使用Serde的SeqDeserializer将序列反序列化为自定义类型?