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

sni*_*n21 5 json rust serde serde-json reqwest

对于 Rust 来说相对较新。我正在尝试进行 API 调用,这需要序列化 ​​JSON 主体。

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;
Run Code Online (Sandbox Code Playgroud)

我正在序列化的结构

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

例如。这里的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?;
Run Code Online (Sandbox Code Playgroud)

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

我读了一些关于为 f32 编写自定义序列化器的文章,它可以截断到小数点后两位,但我对 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)
}
Run Code Online (Sandbox Code Playgroud)

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

use*_*342 5

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

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)
Run Code Online (Sandbox Code Playgroud)

详细解释

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

let lat = 77.63_f32;
Run Code Online (Sandbox Code Playgroud)

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

serde_json工作方式不同 - 它f32通过将值转换为f64序列化值,因为这是 JSON 和 JavaScript 使用的精度。不幸的结果是,10175119/2**17 的近似值77.63_f32被扩大到f64没有存储 77.63 的原始愿望的上下文。简单f64地存储近似值(它可以精确地容纳它,而不会进一步损失精度),当您打印结果 时f64,您会得到77.62999725341797,这就是 10175119/2**17 十进制到 16 位精度的样子。

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

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

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)
Run Code Online (Sandbox Code Playgroud)

操场

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

旁注:上面的代码使用trunc()以下问题中的尝试,但也许round() 如此处所示将是更合适的选择。


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

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)
Run Code Online (Sandbox Code Playgroud)

2 同样使用Python获得:

let lat = 77.63_f32;
Run Code Online (Sandbox Code Playgroud)