json icon indicating copy to clipboard operation
json copied to clipboard

Bug: f64 within flattened HashMap throws error on deserialization

Open smessmer opened this issue 1 year ago • 3 comments

serde = { version = "^1.0.204", features = ["derive"] } serde_json = { version = "^1.0.120", features = ["arbitrary_precision"] }

Code:

#[derive(Serialize, Deserialize)]
struct MainStruct {
    #[serde(flatten)]
    key_value_pairs: HashMap<String, f64>,
}

let obj = MainStruct {
    key_value_pairs: [("key".to_string(), 1.0f64)].into_iter().collect(),
};

let serialized = serde_json::to_string(&obj).unwrap();
println!("{:?}", serialized);
let _parsed: MainStruct = serde_json::from_str(&serialized).unwrap();

This prints the correctly serialized string

{"key":1.0}

But when it tries to deserialize it again, it panics with:

called `Result::unwrap()` on an `Err` value: Error("invalid type: map, expected f64", line: 1, column: 11)

smessmer avatar Jul 25 '24 05:07 smessmer

This seems to be a variation of #959.

purplesyringa avatar Aug 12 '24 19:08 purplesyringa

Here is another variation of the same problem. Without arbitrary_precision, both tests will pass. With arbitrary_precision, the integer test will pass but the real test will fail:

#![allow(dead_code)]
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Test {
    #[serde(flatten)]
    inner: Inner,
}

#[derive(Debug, Deserialize)]
struct Inner {
    value: f64,
}

#[test]
fn flatten_f64_integer() {
    let test: Test = serde_json::from_str(r#"{"value": 1}"#).unwrap();
    dbg!(&test);
}

#[test]
fn flatten_f64_real() {
    let test: Test = serde_json::from_str(r#"{"value": 1.0}"#).unwrap();
    dbg!(&test);
}

blp avatar Aug 05 '25 16:08 blp

I found a workaround. As a diff from the test program I posted above:

diff -u /home/blp/tmp/rusttmp/src/main.rs.broken /home/blp/tmp/rusttmp/src/main.rs
--- /home/blp/tmp/rusttmp/src/main.rs.broken	2025-08-05 10:18:01.282269642 -0700
+++ /home/blp/tmp/rusttmp/src/main.rs	2025-08-05 10:17:04.363135084 -0700
@@ -1,5 +1,9 @@
 #![allow(dead_code)]
-use serde::Deserialize;
+use serde::{
+    de::{DeserializeOwned, Error},
+    Deserialize, Deserializer,
+};
+use serde_json::Value;
 
 #[derive(Debug, Deserialize)]
 struct Test {
@@ -9,9 +13,24 @@
 
 #[derive(Debug, Deserialize)]
 struct Inner {
+    #[serde(deserialize_with = "deserialize_via_value")]
     value: f64,
 }
 
+/// Use this as a serde deserialization function to work around `serde_json`
+/// [issues] with nested `f64`.  It works in two steps, first deserializing a
+/// `serde_json::Value` from `deserializer`, then deserializing `T` from that
+/// `serde_json::Value`.
+///
+/// [issues]: https://github.com/serde-rs/json/issues/1157
+fn deserialize_via_value<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+    D: Deserializer<'de>,
+    T: DeserializeOwned,
+{
+    serde_json::from_value(Value::deserialize(deserializer)?).map_err(D::Error::custom)
+}
+
 #[test]
 fn flatten_f64_integer() {
     let test: Test = serde_json::from_str(r#"{"value": 1}"#).unwrap();

blp avatar Aug 05 '25 17:08 blp