This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11513-d45aaf6b15d527de66ac38330a2e1c4f4f444541 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit 6db2d4fd99c766ecd65a90419cea717ad5fbb619 Author: Peter L <[email protected]> AuthorDate: Sat Oct 17 17:24:23 2026 +0000 fix(parquet-variant-json): preserve whole float formatting (#11513) # Which issue does this PR close? - N/A # Rationale for this change `VariantToJson` writes `Variant::Float` and `Variant::Double` with `Display`, which drops the fractional part of a whole-valued float. `Variant::Double(2.0)` renders as `2`, and `Variant::Float(0.0)` as `0`. The JSON then reads as an integer, so a consumer that distinguishes integers from floats sees a different type, and a JSON string rebuilt from a Variant differs from the JSON the Variant was parsed from. # What changes are included in this PR? - Float and double values are formatted with `ryu`, as `serde_json` and `arrow-json` already do. `2.0` renders as `2.0`. - `ryu` is added to `parquet-variant-json`. It is already a dependency of `arrow-cast` and `arrow-json`, so the dependency tree does not change. `ryu` also uses exponent notation for very large and very small magnitudes, for example `1e21` and `1e-7`, where `Display` writes all the digits. Both forms are valid JSON. # Are these changes tested? Yes. The `to_json` tests now expect `0.0` for `Variant::Float(0.0)`, add a `Variant::Double(2.0)` case, and check that an object with a whole-valued double field renders it as `2.0`. # Are there any significant user-facing changes? Yes. The JSON text for whole-valued floats changes: `2.0` renders as `2.0` instead of `2`. The value is unchanged. Co-authored-by: Matthew Kim <[email protected]> --- Cargo.lock | 1 + parquet-variant-json/Cargo.toml | 1 + parquet-variant-json/src/to_json.rs | 22 +++++++++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 045c6a552f..4d22fedb37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2678,6 +2678,7 @@ dependencies = [ "base64 0.23.1", "chrono", "parquet-variant", + "ryu", "serde_json", "uuid", ] diff --git a/parquet-variant-json/Cargo.toml b/parquet-variant-json/Cargo.toml index d2983f5794..d4f83f34f9 100644 --- a/parquet-variant-json/Cargo.toml +++ b/parquet-variant-json/Cargo.toml @@ -34,6 +34,7 @@ chrono = { workspace = true } serde_json = "1.0" base64 = "0.23" uuid = "1.18.0" +ryu = "1.0" # uuid requires the `js` feature to run on wasm [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/parquet-variant-json/src/to_json.rs b/parquet-variant-json/src/to_json.rs index 5ae19aebb0..930ba935dc 100644 --- a/parquet-variant-json/src/to_json.rs +++ b/parquet-variant-json/src/to_json.rs @@ -175,8 +175,8 @@ impl VariantToJson for Variant<'_, '_> { Variant::Int16(i) => write!(buffer, "{i}")?, Variant::Int32(i) => write!(buffer, "{i}")?, Variant::Int64(i) => write!(buffer, "{i}")?, - Variant::Float(f) => write!(buffer, "{f}")?, - Variant::Double(f) => write!(buffer, "{f}")?, + Variant::Float(f) => write!(buffer, "{}", ryu::Buffer::new().format(*f))?, + Variant::Double(f) => write!(buffer, "{}", ryu::Buffer::new().format(*f))?, Variant::Decimal4(decimal) => write!(buffer, "{decimal}")?, Variant::Decimal8(decimal) => write!(buffer, "{decimal}")?, Variant::Decimal16(decimal) => write!(buffer, "{decimal}")?, @@ -813,8 +813,10 @@ mod tests { JsonTest { variant: Variant::Float(0.0), - expected_json: "0", - expected_value: Value::Number(0.into()), // Use integer 0 to match JSON parsing + expected_json: "0.0", + expected_value: serde_json::Number::from_f64(0.0) + .map(Value::Number) + .unwrap(), } .run(); @@ -836,6 +838,15 @@ mod tests { } .run(); + JsonTest { + variant: Variant::Double(2.0), + expected_json: "2.0", + expected_value: serde_json::Number::from_f64(2.0) + .map(Value::Number) + .unwrap(), + } + .run(); + // Decimals JsonTest { variant: Variant::from(VariantDecimal4::try_new(12345, 2).unwrap()), @@ -1209,7 +1220,7 @@ mod tests { obj.insert("string_field", "test_string"); obj.insert("int_field", 123i32); obj.insert("bool_field", true); - obj.insert("float_field", 2.71f64); + obj.insert("float_field", 2.0f64); obj.insert("null_field", ()); obj.insert("long_field", 999i64); obj.finish(); @@ -1218,6 +1229,7 @@ mod tests { let (metadata, value) = builder.finish(); let variant = Variant::try_new(&metadata, &value)?; let json = variant.to_json_string()?; + assert!(json.contains(r#""float_field":2.0"#)); let parsed: Value = serde_json::from_str(&json).unwrap(); let obj = parsed.as_object().expect("expected JSON object");
