json
json copied to clipboard
Bug: f64 within flattened HashMap throws error on deserialization
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)
This seems to be a variation of #959.
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);
}
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();